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