1 /*
  2  * Utils.js - Core utility routines
  3  *
  4  * Copyright © 2012-2015, 2018-2019, 2021 JEDLSoft
  5  *
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  *
 10  *     http://www.apache.org/licenses/LICENSE-2.0
 11  *
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  *
 16  * See the License for the specific language governing permissions and
 17  * limitations under the License.
 18  */
 19 
 20 var ilib = require("./ilib.js");
 21 var Locale = require("./Locale.js");
 22 var JSUtils = require("./JSUtils.js");
 23 var Path = require("./Path.js");
 24 var ISet = require("./ISet.js");
 25 
 26 var Utils = {};
 27 
 28 /**
 29  * Return the property name inside of ilib.data that corresponds to the given locale data file.
 30  *
 31  * @private
 32  * @param {String} basename the basename of the file
 33  * @param {String} pathname the path from the root to the base file which usually encodes the
 34  * locale of the file
 35  * @param {String=} root the root directory of the file or undefined for the standard locale dir
 36  */
 37 function getPropertyName(basename, pathname, root) {
 38     var bits = [ basename ];
 39     if (root) {
 40         bits = bits.concat(root.split("\/"));
 41     }
 42     if (pathname) {
 43         bits = bits.concat(pathname.split("\/"));
 44     }
 45     return bits.join('_');
 46 }
 47 
 48 /**
 49  * Return an array of locales that represent the sublocales of
 50  * the given locale. These sublocales are intended to be used
 51  * to load locale data. Each sublocale might be represented
 52  * separately by files on disk in order to share them with other
 53  * locales that have the same sublocales. The sublocales are
 54  * given in the order that they should be loaded, which is
 55  * least specific to most specific.<p>
 56  *
 57  * For example, the locale "en-US" would have the sublocales
 58  * "root", "en", "und-US", and "en-US".<p>
 59  *
 60  * <h4>Variations</h4>
 61  *
 62  * With only language and region specified, the following
 63  * sequence of sublocales will be generated:<p>
 64  *
 65  * <pre>
 66  * root
 67  * language
 68  * und-region
 69  * language-region
 70  * </pre>
 71  *
 72  * With only language and script specified:<p>
 73  *
 74  * <pre>
 75  * root
 76  * language
 77  * language-script
 78  * </pre>
 79  *
 80  * With only script and region specified:<p>
 81  *
 82  * <pre>
 83  * root
 84  * und-region
 85  * </pre>
 86  *
 87  * With only region and variant specified:<p>
 88  *
 89  * <pre>
 90  * root
 91  * und-region
 92  * region-variant
 93  * </pre>
 94  *
 95  * With only language, script, and region specified:<p>
 96  *
 97  * <pre>
 98  * root
 99  * language
100  * und-region
101  * language-script
102  * language-region
103  * language-script-region
104  * </pre>
105  *
106  * With only language, region, and variant specified:<p>
107  *
108  * <pre>
109  * root
110  * language
111  * und-region
112  * language-region
113  * und-region-variant
114  * language-region-variant
115  * </pre>
116  *
117  * With all parts specified:<p>
118  *
119  * <pre>
120  * root
121  * language
122  * und-region
123  * language-script
124  * language-region
125  * und-region-variant
126  * language-script-region
127  * language-region-variant
128  * language-script-region-variant
129  * </pre>
130  *
131  * @static
132  * @param {Locale|String} locale the locale to find the sublocales for
133  * @return {Array.<string>} An array of locale specifiers that
134  * are the sublocales of the given on
135  */
136 Utils.getSublocales = function(locale) {
137     var ret = ["root"];
138     var loc = typeof(locale) === "string" ? new Locale(locale) : locale;
139     var lang = loc.getLanguage();
140     var region = loc.getRegion();
141     var script = loc.getScript();
142     var variant = loc.getVariant();
143 
144     if (lang) {
145         ret.push(lang);
146     }
147     if (region) {
148         ret.push('und-' + region);
149     }
150 
151     if (lang) {
152         if (script) {
153             ret.push(lang + '-' + script);
154         }
155         if (region) {
156             ret.push(lang + '-' + region);
157         }
158         if (variant) {
159             ret.push(lang + '-' + variant);
160         }
161     }
162 
163     if (region && variant) {
164         ret.push("und-" + region + '-' + variant);
165     }
166 
167     if (lang) {
168         if (script && region) {
169             ret.push(lang + '-' + script + '-' + region);
170         }
171         if (script && variant) {
172             ret.push(lang + '-' + script + '-' + variant);
173         }
174         if (region && variant) {
175             ret.push(lang + '-' + region + '-' + variant);
176         }
177         if (script && region && variant) {
178             ret.push(lang + '-' + script + '-' + region + '-' + variant);
179         }
180     }
181     return ret;
182 };
183 
184 /**
185  * Find and merge all the locale data for a particular prefix in the given locale
186  * and return it as a single javascript object. This merges the data in the
187  * correct order:
188  *
189  * <ol>
190  * <li>shared data (usually English)
191  * <li>data for language
192  * <li>data for language + region
193  * <li>data for language + region + script
194  * <li>data for language + region + script + variant
195  * </ol>
196  *
197  * It is okay for any of the above to be missing. This function will just skip the
198  * missing data.
199  *
200  * @static
201  * @param {string} prefix prefix under ilib.data of the data to merge
202  * @param {Locale} locale locale of the data being sought
203  * @param {boolean=} replaceArrays if true, replace the array elements in object1 with those in object2.
204  * If false, concatenate array elements in object1 with items in object2.
205  * @param {boolean=} returnOne if true, only return the most locale-specific data. If false,
206  * merge all the relevant locale data together.
207  * @param {string=} root root path if there is one
208  * @return {Object?} the merged locale data
209  */
210 Utils.mergeLocData = function (prefix, locale, replaceArrays, returnOne, root) {
211     var data = undefined;
212     var loc = locale || new Locale();
213     var mostSpecific;
214 
215     data = {};
216 
217     mostSpecific = data;
218 
219     Utils.getSublocales(loc).forEach(function(l) {
220         var property = getPropertyName(prefix, (l === "root") ? undefined : l.replace(/-/g, "/"), root);
221 
222         if (ilib.data[property]) {
223             if (returnOne) {
224                 mostSpecific = ilib.data[property];
225             } else {
226                 data = JSUtils.merge(data, ilib.data[property], replaceArrays);
227             }
228         }
229     });
230 
231     return returnOne ? mostSpecific : data;
232 };
233 
234 
235 /**
236  * Return an array of relative path names for the
237  * files that represent the data for the given locale.<p>
238  *
239  * Note that to prevent the situation where a directory for
240  * a language exists next to the directory for a region where
241  * the language code and region code differ only by case, the
242  * plain region directories are located under the special
243  * "undefined" language directory which has the ISO code "und".
244  * The reason is that some platforms have case-insensitive
245  * file systems, and you cannot have 2 directories with the
246  * same name which only differ by case. For example, "es" is
247  * the ISO 639 code for the language "Spanish" and "ES" is
248  * the ISO 3166 code for the region "Spain", so both the
249  * directories cannot exist underneath "locale". The region
250  * therefore will be loaded from "und/ES" instead.<p>
251  *
252  * <h4>Variations</h4>
253  *
254  * With only language and region specified, the following
255  * sequence of paths will be generated:<p>
256  *
257  * <pre>
258  * language
259  * und/region
260  * language/region
261  * </pre>
262  *
263  * With only language and script specified:<p>
264  *
265  * <pre>
266  * language
267  * language/script
268  * </pre>
269  *
270  * With only script and region specified:<p>
271  *
272  * <pre>
273  * und/region
274  * </pre>
275  *
276  * With only region and variant specified:<p>
277  *
278  * <pre>
279  * und/region
280  * region/variant
281  * </pre>
282  *
283  * With only language, script, and region specified:<p>
284  *
285  * <pre>
286  * language
287  * und/region
288  * language/script
289  * language/region
290  * language/script/region
291  * </pre>
292  *
293  * With only language, region, and variant specified:<p>
294  *
295  * <pre>
296  * language
297  * und/region
298  * language/region
299  * region/variant
300  * language/region/variant
301  * </pre>
302  *
303  * With all parts specified:<p>
304  *
305  * <pre>
306  * language
307  * und/region
308  * language/script
309  * language/region
310  * region/variant
311  * language/script/region
312  * language/region/variant
313  * language/script/region/variant
314  * </pre>
315  *
316  * @static
317  * @param {Locale} locale load the files for this locale
318  * @param {string?} name the file name of each file to load without
319  * any path
320  * @return {Array.<string>} An array of relative path names
321  * for the files that contain the locale data
322  */
323 Utils.getLocFiles = function(locale, name) {
324     var filename = name || "resources.json";
325     var loc = locale || new Locale();
326 
327     return Utils.getSublocales(loc).map(function(l) {
328         return (l === "root") ? filename : Path.join(l.replace(/-/g, "/"), filename);
329     });
330 };
331 
332 /**
333  * Load data using the new loader object or via the old function callback.
334  * @static
335  * @private
336  */
337 Utils._callLoadData = function (files, sync, params, root, callback) {
338     // console.log("Utils._callLoadData called");
339     if (typeof(ilib._load) === 'function') {
340         // console.log("Utils._callLoadData: calling as a regular function");
341         return ilib._load(files, sync, params, callback);
342     } else if (typeof(ilib._load) === 'object' && typeof(ilib._load.loadFiles) === 'function') {
343         // console.log("Utils._callLoadData: calling as an object");
344         return ilib._load.loadFiles(files, sync, params, callback, root);
345     }
346 
347     // console.log("Utils._callLoadData: not calling. Type is " + typeof(ilib._load) + " and instanceof says " + (ilib._load instanceof Loader));
348     return undefined;
349 };
350 
351 function getPropertyNameFromFile(basename, filepath, root) {
352     var dir = Path.dirname(filepath);
353     return getPropertyName(basename, (dir === "." || dir === "/" || dir === "..") ? undefined : dir, root);
354 }
355 
356 /**
357  * Return true if the locale data corresponding to the given pathname is not already loaded
358  * or assembled.
359  *
360  * @private
361  * @param basename
362  * @param locale
363  * @returns
364  */
365 function dataNotExists(basename, pathname, root) {
366     return !ilib.data[getPropertyNameFromFile(basename, pathname, root)];
367 }
368 
369 /**
370  * Find locale data or load it in. If the data with the given name is preassembled, it will
371  * find the data in ilib.data. If the data is not preassembled but there is a loader function,
372  * this function will call it to load the data. Otherwise, the callback will be called with
373  * undefined as the data. This function will create a cache under the given class object.
374  * If data was successfully loaded, it will be set into the cache so that future access to
375  * the same data for the same locale is much quicker.<p>
376  *
377  * The parameters can specify any of the following properties:<p>
378  *
379  * <ul>
380  * <li><i>name</i> - String. The name of the file being loaded. Default: ResBundle.json
381  * <li><i>object</i> - String. The name of the class attempting to load data. This is used to differentiate parts of the cache.
382  * <li><i>locale</i> - Locale. The locale for which data is loaded. Default is the current locale.
383  * <li><i>nonlocale</i> - boolean. If true, the data being loaded is not locale-specific.
384  * <li><i>type</i> - String. Type of file to load. This can be "json" or "other" type. Default: "json"
385  * <li><i>replace</i> - boolean. When merging json objects, this parameter controls whether to merge arrays
386  * or have arrays replace each other. If true, arrays in child objects replace the arrays in parent
387  * objects. When false, the arrays in child objects are concatenated with the arrays in parent objects.
388  * <li><i>root</i> - String. If provided, look in this root directory first for files, and then fall back
389  * to the standard include paths if they are not found in this root. If not provided, just search the
390  * standard include paths.
391  * <li><i>loadParams</i> - Object. An object with parameters to pass to the loader function
392  * <li><i>sync</i> - boolean. Whether or not to load the data synchronously
393  * <li><i>callback</i> - function(?)=. callback Call back function to call when the data is available.
394  * Data is not returned from this method, so a callback function is mandatory.
395  * </ul>
396  *
397  * @static
398  * @param {Object} params Parameters configuring how to load the files (see above)
399  */
400 Utils.loadData = function(params) {
401     var name = "resources.json",
402         locale = new Locale(ilib.getLocale()),
403         sync = false,
404         type = undefined,
405         loadParams = {},
406         callback = undefined,
407         nonlocale = false,
408         replace = false,
409         root,
410         basename;
411 
412     if (!params || typeof(params.callback) !== 'function') {
413         throw "Utils.loadData called without a callback. It must have a callback to work.";
414     }
415 
416     if (params.name) {
417         name = params.name;
418     }
419     if (params.locale) {
420         locale = (typeof(params.locale) === 'string') ? new Locale(params.locale) : params.locale;
421     }
422     if (params.type) {
423         type = params.type;
424     }
425     if (params.loadParams) {
426         loadParams = params.loadParams;
427     }
428     if (params.sync) {
429         sync = params.sync;
430     }
431     if (params.nonlocale) {
432         nonlocale = !!params.nonlocale;
433     }
434     if (typeof(params.replace) === 'boolean') {
435         replace = params.replace;
436     }
437 
438     root = params.root;
439     callback = params.callback;
440 
441     if (!type) {
442         var dot = name.lastIndexOf(".");
443         type = (dot !== -1) ? name.substring(dot+1) : "text";
444     }
445 
446     if (typeof(ilib.data.cache) === "undefined") {
447         ilib.data.cache = {};
448     }
449     if (typeof(ilib.data.cache.fileSet) === "undefined") {
450         ilib.data.cache.fileSet = new ISet();
451     }
452 
453     var data, returnOne = ((loadParams && loadParams.returnOne) || type !== "json");
454 
455     basename = name.substring(0, name.lastIndexOf(".")).replace(/[\.:\(\)\/\\\+\-]/g, "_");
456 
457     if (ilib._cacheMerged) {
458         if (typeof(ilib.data.cache.merged) === "undefined") {
459             ilib.data.cache.merged = {};
460         }
461         var spec = ((!nonlocale && locale.getSpec().replace(/-/g, '_')) || "root") + "," + basename + "," + String(JSUtils.hashCode(loadParams));
462         if (typeof(ilib.data.cache.merged[spec]) !== 'undefined') {
463             // cache hit!
464             callback(ilib.data.cache.merged[spec]);
465             return;
466         }
467     }
468 
469     if (typeof(ilib._load) !== 'undefined') {
470         // We have a loader, so we can figure out which json files are loaded already and
471         // which are not so that we can load the missing ones.
472         // the data is not preassembled, so attempt to load it dynamically
473         var files = nonlocale ? [ name || "resources.json" ] : Utils.getLocFiles(locale, name);
474 
475         var isPath = ilib._load.isMultiPaths;
476         
477         if (typeof(isPath) === "undefined" || isPath === false){
478             // find the ones we haven't loaded before
479             files = files.filter(ilib.bind(this, function(file) {
480                 return !ilib.data.cache.fileSet.has(Path.join(root, file)) && dataNotExists(basename, file, root);
481             }));
482         }
483 
484         if (files.length) {
485             Utils._callLoadData(files, sync, loadParams, root, ilib.bind(this, function(arr) {
486                 for (var i = 0; i < files.length; i++) {
487                     if (arr[i]) {
488                         var property = nonlocale ? basename : getPropertyNameFromFile(basename, files[i], root);
489 
490                         if (isPath || !ilib.data[property]) {
491                             ilib.data[property] = arr[i];
492                         }
493                     }
494                     
495                     ilib.data.cache.fileSet.add(Path.join(root, files[i]));
496                 }
497 
498                 if (!nonlocale) {
499                     data = Utils.mergeLocData(basename, locale, replace, returnOne, root);
500                     if (ilib._cacheMerged) ilib.data.cache.merged[spec] = data;
501                 } else {
502                     data = ilib.data[basename];
503                 }
504 
505                 callback(data);
506             }));
507 
508             return;
509         }
510         // otherwise the code below will return the already-loaded data
511     }
512 
513     // No loader, or data already loaded? Then use whatever data we have already in ilib.data
514     if (!nonlocale) {
515         data = Utils.mergeLocData(basename, locale, replace, returnOne, root);
516         if (ilib._cacheMerged) ilib.data.cache.merged[spec] = data;
517     } else {
518         data = ilib.data[basename];
519     }
520 
521     callback(data);
522 };
523 
524 module.exports = Utils;
525