1 /*
2  * Copyright (c) 2021-2024 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 package ohos.global.i18n;
17 
18 import com.ibm.icu.text.DateFormatSymbols;
19 import com.ibm.icu.text.DateTimePatternGenerator;
20 import com.ibm.icu.text.DecimalFormatSymbols;
21 import com.ibm.icu.text.NumberFormat;
22 import com.ibm.icu.text.NumberingSystem;
23 import com.ibm.icu.util.ULocale;
24 import com.ibm.icu.text.DateFormat;
25 import com.ibm.icu.text.SimpleDateFormat;
26 import com.ibm.icu.util.Calendar;
27 
28 import java.util.ArrayList;
29 import java.util.HashMap;
30 import java.util.Objects;
31 import java.util.concurrent.locks.ReentrantLock;
32 import java.util.Map;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
35 import java.lang.reflect.Field;
36 import java.lang.reflect.InvocationTargetException;
37 import java.lang.reflect.Method;
38 
39 import ohos.global.i18n.ResourceConfiguration.ConfigItem;
40 import ohos.global.i18n.ResourceConfiguration.Element;
41 
42 /**
43  * Fetcher is used to fetche a locale's specified data.
44  *
45  * @since 2022-8-22
46  */
47 public class Fetcher implements Runnable, Comparable<Fetcher> {
48     /** configuration extracted from resourec_items.json */
49     private static ArrayList<ConfigItem> configItems = null;
50     private static final Logger LOG = Logger.getLogger("Fetcher");
51     private static int resourceCount = 0;
52     private static HashMap<Integer, String> int2Str = new HashMap<>();
53     private static HashMap<String, Integer> str2Int = new HashMap<>();
54     private static boolean sStatusOk = true;
55 
56     static {
57         configItems = ResourceConfiguration.parse();
configItems.sort(ConfigItem first, ConfigItem second) -> first.getIndex() - second.getIndex()58         configItems.sort((ConfigItem first, ConfigItem second) -> first.getIndex() - second.getIndex());
59         resourceCount = configItems.size();
60     }
61 
62     /**
63      * Used to store data related to a locale.
64      */
65     public final ArrayList<String> datas = new ArrayList<>();
66 
67     /**
68      * All non-repeated strings will be put into idMap.
69      */
70     public final Map<String, Integer> idMap;
71 
72     /**
73      * LanguageTag related to the locale.
74      */
75     public final String languageTag;
76 
77     // Indicate whether this Fetcher is included in the final generation process of i18n.dat file.
78     private boolean included = true;
79     private String lan; // language
80     private ReentrantLock lock; // Lock used to synchronize dump operation
81     private ULocale locale;
82     private DateFormatSymbols formatSymbols;
83     private DateTimePatternGenerator patternGenerator;
84     private int status = 0;
85     private String defaultHourString;
86     private ArrayList<Integer> reserved = new ArrayList<>();
87 
88     /**
89      * Constructor of class Fetcher.
90      *
91      * @param tag language tag
92      * @param lock used to synchronize dump operation
93      * @param idMap all non-repeated strings will be put into idMap.
94      */
Fetcher(String tag, ReentrantLock lock, Map<String, Integer> idMap)95     public Fetcher(String tag, ReentrantLock lock, Map<String, Integer> idMap) {
96         if (!Utils.isValidLanguageTag(tag)) {
97             LOG.log(Level.SEVERE, String.format("wrong languageTag %s", tag));
98             status = 1;
99         }
100         this.languageTag = tag;
101         Objects.requireNonNull(lock);
102         this.lock = lock;
103         Objects.requireNonNull(idMap);
104         this.idMap = idMap;
105         this.lan = this.languageTag.split("-")[0];
106         this.locale = ULocale.forLanguageTag(this.languageTag);
107         formatSymbols = DateFormatSymbols.getInstance(locale);
108         patternGenerator = DateTimePatternGenerator.getInstance(locale);
109         defaultHourString = defaultHour();
110     }
111 
112     /**
113      * show whether resouce_items is loaded successfully
114      *
115      * @return true if status is right, otherwise false
116      */
isFetcherStatusOk()117     public static boolean isFetcherStatusOk() {
118         return sStatusOk;
119     }
120 
121     /**
122      * return the total resource number
123      *
124      * @return resourceCount
125      */
getResourceCount()126     public static int getResourceCount() {
127         return resourceCount;
128     }
129 
130     /**
131      * Methods to get int2Str
132      *
133      * @return Return int2Str
134      */
getInt2Str()135     public static HashMap<Integer, String> getInt2Str() {
136         return int2Str;
137     }
138 
139     /**
140      * Methods to get str2Int
141      *
142      * @return Return str2Int
143      */
getStr2Int()144     public static HashMap<String, Integer> getStr2Int() {
145         return str2Int;
146     }
147 
148     /**
149      * Check the status of the fetcher, normally a wrong language tag
150      * can make the status wrong.
151      *
152      * @return the status
153      */
checkStatus()154     public boolean checkStatus() {
155         return status == 0;
156     }
157 
158     /**
159      * Get all meta data defined in resource_items.json
160      */
getData()161     public void getData() {
162         int current = 0;
163         Method method = null;
164         for (ConfigItem item : configItems) {
165             int index = item.getIndex();
166             if (current != index) {
167                 throw new IllegalStateException();
168             }
169             String methodString = item.getMethod();
170             try {
171                 method = Fetcher.class.getDeclaredMethod(methodString, ConfigItem.class);
172                 method.setAccessible(true);
173                 method.invoke(this, item);
174             } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
175                 LOG.severe("get data failed for index " + current);
176             }
177             ++current;
178         }
179     }
180 
181     /**
182      * Get included.
183      *
184      * @return If this Fetcher is included in the final generation process of i18n.dat file.
185      */
getIncluded()186     public boolean getIncluded() {
187         return included;
188     }
189 
190     /**
191      * Set included.
192      *
193      * @param val this Fetcher is included in the final generation process of i18n.dat file.
194      */
setIncluded(boolean val)195     public void setIncluded(boolean val) {
196         included = val;
197     }
198 
199     /**
200      * Dump all datas in this locale to idMap
201      */
dump()202     public void dump() {
203         try {
204             lock.lock();
205             int size = this.idMap.size();
206             for (int i = 0; i < datas.size(); i++) {
207                 String data = datas.get(i);
208                 if (!idMap.containsKey(data)) {
209                     idMap.put(data, size);
210                     size++;
211                 }
212             }
213         } finally {
214             lock.unlock();
215         }
216     }
217 
218     /**
219      * Equals function to determine whether two objs are equal
220      *
221      * @param obj Object to be compared
222      * @return Return true if obj is equals to this Fetcher object, otherwise false
223      */
equals(Object obj)224     public boolean equals(Object obj) {
225         if (!(obj instanceof Fetcher)) {
226             return false;
227         }
228         Fetcher fetcher = (Fetcher) obj;
229         if (datas.size() != fetcher.datas.size()) {
230             return false;
231         }
232         for (int i = 0; i < datas.size(); i++) {
233             if (!datas.get(i).equals(fetcher.datas.get(i))) {
234                 return false;
235             }
236         }
237         return true;
238     }
239 
240     /**
241      * Returns hashcode
242      *
243      * @return HashCode of Fetcher object
244      */
245     @Override
hashCode()246     public int hashCode() {
247         return datas.hashCode() + languageTag.hashCode();
248     }
249 
250     /**
251      * Override methods in Runnable
252      */
run()253     public void run() {
254         getData();
255         dump();
256     }
257 
258     /**
259      * the i'th value represent whether i'th resource should reserved for this locale
260      *
261      * @param index resource id
262      * @return 1 represents i'th resource reserved.
263      */
reservedGet(int index)264     public int reservedGet(int index) {
265         return reserved.get(index);
266     }
267 
268     /**
269      * the i'th value represent whether i'th resource should reserved for this locale
270      *
271      * @param val whether reserves current resource
272      */
reservedAdd(int val)273     public void reservedAdd(int val) {
274         reserved.add(val);
275     }
276 
convertNoAscii(String str)277     private String convertNoAscii(String str) {
278         return str;
279     }
280 
281     // Get month names
getMonthNames(int formatType, int lengthType)282     private void getMonthNames(int formatType, int lengthType) {
283         StringBuilder sb = new StringBuilder();
284         String[] months = formatSymbols.getMonths(formatType, lengthType);
285         for (int i = 0; i < months.length; i++) {
286             sb.append(months[i]);
287             if (i != months.length - 1) {
288                 sb.append(FileConfig.SEP);
289             }
290         }
291         datas.add(sb.toString());
292     }
293 
294     // Get weekday names
getWeekDayNames(int formatType, int lengthType)295     private void getWeekDayNames(int formatType, int lengthType) {
296         StringBuilder sb = new StringBuilder();
297         String[] weekdays = formatSymbols.getWeekdays(formatType, lengthType);
298         String[] adjustWeekdays = new String[(weekdays.length - 1)];
299         for (int i = 0; i < adjustWeekdays.length; i++) {
300             adjustWeekdays[i] = weekdays[i + 1];
301         }
302         for (int i = 0; i < adjustWeekdays.length; i++) {
303             sb.append(adjustWeekdays[i]);
304             if (i != adjustWeekdays.length - 1) {
305                 sb.append(FileConfig.SEP);
306             }
307         }
308         this.datas.add(sb.toString());
309     }
310 
getPatterns(ConfigItem config)311     private void getPatterns(ConfigItem config) {
312         if (config.elements == null) {
313             throw new IllegalArgumentException("no patterns defined in resource_items.json for index: " + config.index);
314         }
315         Element[] elements = config.elements;
316         int current = 0;
317         ArrayList<String> skeletons = new ArrayList<String>();
318         for (Element ele : elements) {
319             int index = ele.index;
320             if (current != index) {
321                 throw new IllegalStateException("wrong index order in patterns for index: " + config.index);
322             }
323             ++current;
324             skeletons.add(ele.skeleton);
325         }
326         StringBuilder sb = new StringBuilder();
327         String[] outPatterns = new String[skeletons.size()];
328         processPatterns(outPatterns, skeletons);
329         for (int i = 0; i < skeletons.size(); i++) {
330             sb.append(outPatterns[i]);
331             if (i != outPatterns.length - 1) {
332                 sb.append(FileConfig.SEP);
333             }
334         }
335         datas.add(sb.toString());
336     }
337 
processPatterns(String[] outPatterns, ArrayList<String> skeletons)338     private void processPatterns(String[] outPatterns, ArrayList<String> skeletons) {
339         for (int i = 0; i < skeletons.size(); ++i) {
340             switch (skeletons.get(i)) {
341                 case "FULL":
342                 case "MEDIUM":
343                 case "SHORT": {
344                     outPatterns[i] = getFMSPattern(skeletons.get(i));
345                     break;
346                 }
347                 default: {
348                     processSpecialPattern(outPatterns, skeletons, i);
349                 }
350             }
351         }
352     }
353 
processSpecialPattern(String[] outPatterns, ArrayList<String> skeletons, int i)354     private void processSpecialPattern(String[] outPatterns, ArrayList<String> skeletons, int i) {
355         if ("en-US".equals(languageTag) && ("Ed".equals(skeletons.get(i)))) {
356             outPatterns[i] = "EEE d";
357             return;
358         }
359         if ("jm".equals(skeletons.get(i))) {
360             if ("h".equals(defaultHourString)) {
361                 outPatterns[i] = patternGenerator.getBestPattern("hm");
362             } else {
363                 outPatterns[i] = patternGenerator.getBestPattern("Hm");
364             }
365             return;
366         }
367         if ("jms".equals(skeletons.get(i))) {
368             if ("h".equals(defaultHourString)) {
369                 outPatterns[i] = patternGenerator.getBestPattern("hms");
370             } else {
371                 outPatterns[i] = patternGenerator.getBestPattern("Hms");
372             }
373             return;
374         }
375         outPatterns[i] = patternGenerator.getBestPattern(skeletons.get(i));
376     }
377 
378     // Get FULL-MEDIUM_SHORT pattern
getFMSPattern(String skeleton)379     private String getFMSPattern(String skeleton) {
380         DateFormat formatter = null;
381         try {
382             Field patternField = DateFormat.class.getField(skeleton);
383             int patternIndex = patternField.getInt(null);
384             formatter = DateFormat.getDateInstance(patternIndex, locale);
385         } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
386             LOG.log(Level.SEVERE, "cannot get field " + skeleton);
387         }
388         if (formatter instanceof SimpleDateFormat) {
389             return ((SimpleDateFormat) formatter).toPattern();
390         } else {
391             LOG.log(Level.SEVERE, "wrong type in getFMSPattern");
392             return "";
393         }
394     }
395 
396     // 0. get format abbreviated month names
getFormatAbbrMonthNames(ConfigItem config)397     private void getFormatAbbrMonthNames(ConfigItem config) {
398         getMonthNames(DateFormatSymbols.FORMAT, DateFormatSymbols.ABBREVIATED);
399     }
400 
401     // 1. get format abbreviated day names
getFormatAbbrDayNames(ConfigItem config)402     private void getFormatAbbrDayNames(ConfigItem config) {
403         getWeekDayNames(DateFormatSymbols.FORMAT, DateFormatSymbols.ABBREVIATED);
404     }
405 
406     // 4. get am pm markser
getAmPmMarkers(ConfigItem config)407     private void getAmPmMarkers(ConfigItem config) {
408         StringBuilder sb = new StringBuilder();
409         String[] amPmStrings = formatSymbols.getAmPmStrings();
410         for (int i = 0; i < amPmStrings.length; ++i) {
411             sb.append(amPmStrings[i]);
412             if (i != amPmStrings.length - 1) {
413                 sb.append(FileConfig.SEP);
414             }
415         }
416         this.datas.add(sb.toString());
417     }
418 
419     // 5. get plural data
getPluralRules(ConfigItem config)420     private void getPluralRules(ConfigItem config) {
421         String str = PluralFetcher.getInstance().get(this.lan);
422         if (str == null) {
423             str = "";
424         }
425         this.datas.add(str);
426     }
427 
getDecimalPluralRules(ConfigItem config)428     private void getDecimalPluralRules(ConfigItem config) {
429         String str = PluralFetcher.getInstance().getDecimal(this.lan);
430         if (str == null) {
431             str = "";
432         }
433         this.datas.add(str);
434     }
435 
436     // 6. get number format data
437     @SuppressWarnings("Deprecation")
getNumberFormat(ConfigItem config)438     private void getNumberFormat(ConfigItem config) {
439         StringBuilder sb = new StringBuilder();
440         String pattern = NumberFormat.getPatternForStyle(locale, NumberFormat.NUMBERSTYLE);
441         String percentPattern = NumberFormat.getPatternForStyle(locale, NumberFormat.PERCENTSTYLE);
442         DecimalFormatSymbols decimalFormatSymbols = new DecimalFormatSymbols(locale);
443         String decimalSeparator = decimalFormatSymbols.getDecimalSeparatorString();
444         String groupingSeparator = decimalFormatSymbols.getGroupingSeparatorString();
445         String percent = decimalFormatSymbols.getPercentString();
446         sb.append(pattern);
447         sb.append(FileConfig.SEP);
448         sb.append(percentPattern);
449         sb.append(FileConfig.SEP);
450         sb.append(convertNoAscii(decimalSeparator));
451         sb.append(FileConfig.SEP);
452         sb.append(convertNoAscii(groupingSeparator));
453         sb.append(FileConfig.SEP);
454         sb.append(convertNoAscii(percent));
455         datas.add(sb.toString());
456     }
457 
458     // 7. get number digits
getNumberDigits(ConfigItem config)459     private void getNumberDigits(ConfigItem config) {
460         NumberingSystem numberSystem = NumberingSystem.getInstance(locale);
461         String description = numberSystem.getDescription();
462         StringBuilder sb = new StringBuilder();
463         for (int i = 0; i < description.length(); i++) {
464             sb.append(String.valueOf(description.charAt(i)));
465             if (i != description.length() - 1) {
466                 sb.append(FileConfig.NUMBER_SEP);
467             }
468         }
469         datas.add(sb.toString());
470     }
471 
472     // 8. get time separtor
473     @SuppressWarnings("Deprecation")
getTimeSeparator(ConfigItem config)474     private void getTimeSeparator(ConfigItem config) {
475         datas.add(formatSymbols.getTimeSeparatorString());
476     }
477 
478     // 9. get default hour
getDefaultHour(ConfigItem config)479     private void getDefaultHour(ConfigItem config) {
480         datas.add(defaultHourString);
481     }
482 
483     // 10.get standalone abbreviated month
getStandAloneAbbrMonthNames(ConfigItem config)484     private void getStandAloneAbbrMonthNames(ConfigItem config) {
485         getMonthNames(DateFormatSymbols.STANDALONE, DateFormatSymbols.ABBREVIATED);
486     }
487 
488     // 11. get standalone abbreviated weekday
getStandAloneAbbrWeekDayNames(ConfigItem config)489     private void getStandAloneAbbrWeekDayNames(ConfigItem config) {
490         getWeekDayNames(DateFormatSymbols.STANDALONE, DateFormatSymbols.ABBREVIATED);
491     }
492 
493     // 12. get format wide month
getFormatWideMonthNames(ConfigItem config)494     private void getFormatWideMonthNames(ConfigItem config) {
495         getMonthNames(DateFormatSymbols.FORMAT, DateFormatSymbols.WIDE);
496     }
497 
498     // 13. get format wide days
getFormatWideWeekDayNames(ConfigItem config)499     private void getFormatWideWeekDayNames(ConfigItem config) {
500         getWeekDayNames(DateFormatSymbols.FORMAT, DateFormatSymbols.WIDE);
501     }
502 
503     // 14. get standalone wide days
getStandAloneWideWeekDayNames(ConfigItem config)504     private void getStandAloneWideWeekDayNames(ConfigItem config) {
505         getWeekDayNames(DateFormatSymbols.STANDALONE, DateFormatSymbols.WIDE);
506     }
507 
508     // 15. get standalone wide month
getStandAloneWideMonthNames(ConfigItem config)509     private void getStandAloneWideMonthNames(ConfigItem config) {
510         getMonthNames(DateFormatSymbols.STANDALONE, DateFormatSymbols.WIDE);
511     }
512 
513     // 16. get measure format pattern
getMeasureFormatPatterns(ConfigItem config)514     private void getMeasureFormatPatterns(ConfigItem config) {
515         String str = MeasureFormatPatternFetcher.getInstance().get(this.languageTag);
516         if (str == null) {
517             str = "";
518         }
519         this.datas.add(str);
520     }
521 
defaultHour()522     private String defaultHour() {
523         DateFormat tempFormat = DateFormat
524             .getTimeInstance(DateFormat.SHORT, ULocale.forLanguageTag(languageTag));
525         SimpleDateFormat timeInstance = null;
526         if (tempFormat instanceof SimpleDateFormat) {
527             timeInstance = (SimpleDateFormat) tempFormat;
528         }
529         String shortDateTimePattern = (timeInstance == null) ? "" : timeInstance.toPattern();
530         if (shortDateTimePattern.contains("H")) {
531             return "H";
532         } else {
533             return "h";
534         }
535     }
536 
getWeekdata(ConfigItem config)537     private void getWeekdata(ConfigItem config) {
538         Calendar cal = Calendar.getInstance(ULocale.forLanguageTag(languageTag));
539         Calendar.WeekData weekdata = cal.getWeekData();
540         StringBuilder sb = new StringBuilder();
541         sb.append(weekdata.firstDayOfWeek);
542         sb.append(FileConfig.NUMBER_SEP);
543         sb.append(weekdata.minimalDaysInFirstWeek);
544         sb.append(FileConfig.NUMBER_SEP);
545         sb.append(weekdata.weekendOnset);
546         sb.append(FileConfig.NUMBER_SEP);
547         sb.append(weekdata.weekendCease);
548         datas.add(sb.toString());
549     }
550 
getMinusSign(ConfigItem config)551     private void getMinusSign(ConfigItem config) {
552         NumberFormat formatter = NumberFormat.getNumberInstance(locale);
553         String formatValue = formatter.format(-1);
554         NumberingSystem numberSystem = NumberingSystem.getInstance(locale);
555         String description = numberSystem.getDescription();
556         if (formatValue.length() > 0) {
557             String temp = formatValue.substring(0, formatValue.indexOf(description.charAt(1)));
558             datas.add(temp);
559         }
560     }
561 
562     @Override
compareTo(Fetcher other)563     public int compareTo(Fetcher other) {
564         if (languageTag == null && other.languageTag == null) {
565             return 0;
566         }
567         if (languageTag == null) {
568             return -1;
569         } else if (other.languageTag == null) {
570             return 1;
571         } else {
572             return languageTag.compareTo(other.languageTag);
573         }
574     }
575 }
576