001/**
002 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003 *
004 * Copyright (c) 2006 Sun Microsystems Inc. All Rights Reserved
005 *
006 * The contents of this file are subject to the terms
007 * of the Common Development and Distribution License
008 * (the License). You may not use this file except in
009 * compliance with the License.
010 *
011 * You can obtain a copy of the License at
012 * https://opensso.dev.java.net/public/CDDLv1.0.html or
013 * opensso/legal/CDDLv1.0.txt
014 * See the License for the specific language governing
015 * permission and limitations under the License.
016 *
017 * When distributing Covered Code, include this CDDL
018 * Header Notice in each file and include the License file
019 * at opensso/legal/CDDLv1.0.txt.
020 * If applicable, add the following below the CDDL Header,
021 * with the fields enclosed by brackets [] replaced by
022 * your own identifying information:
023 * "Portions Copyrighted [year] [name of copyright owner]"
024 *
025 * $Id: Locale.java,v 1.7 2009/07/07 17:32:02 bina Exp $
026 *
027 */
028
029/*
030 * Portions Copyrighted 2014-2015 ForgeRock AS.
031 * Portions Copyrighted 2014 Nomura Research Institute, Ltd.
032 */
033package com.sun.identity.shared.locale;
034
035import com.sun.identity.shared.Constants;
036import com.sun.identity.shared.configuration.SystemPropertiesManager;
037import com.sun.identity.shared.debug.Debug;
038import java.io.UnsupportedEncodingException;
039import java.text.MessageFormat;
040import java.text.ParsePosition;
041import java.text.SimpleDateFormat;
042import java.util.BitSet;
043import java.util.Date;
044import java.util.ResourceBundle;
045import java.util.StringTokenizer;
046
047/**
048 * This class <code>Locale.java</code> is a utility that provides
049 * functionality for applications and services to internationalize their
050 * messages.
051 * @supported.all.api
052 */
053public class Locale {
054    static BitSet dontEncode;
055
056    static final int caseDiff = ('a' - 'A');
057
058    private static final int LOCALE_STRING_MAX_LEN = 5;
059
060    static java.util.Locale defaultLocale;
061
062    static Debug debug;
063
064    protected static final String USER_PROPERTIES = "amUser";
065
066    protected static final String DATE_SYNTAX = "dateSyntax";
067
068    private static final String normalizedDateString = "yyyy/MM/dd HH:mm:ss";
069
070    private static final SimpleDateFormat normalizedDateFormat;
071
072    private static final String UNDERSCORE = "_";
073
074    private static final String HYPHEN = "-";
075
076
077    /*
078     * The list of characters that are not encoded have been determined by
079     * referencing O'Reilly's "HTML: The Definitive Guide" (page 164).
080     */
081
082    static {
083        // Intialize static variables
084        debug = Debug.getInstance("amUtil");
085
086        dontEncode = new BitSet(256);
087        int i;
088        for (i = 'a'; i <= 'z'; i++) {
089            dontEncode.set(i);
090        }
091        for (i = 'A'; i <= 'Z'; i++) {
092            dontEncode.set(i);
093        }
094        for (i = '0'; i <= '9'; i++) {
095            dontEncode.set(i);
096        }
097        dontEncode.set(' '); /*
098                                 * encoding a space to a + is done in the
099                                 * encode() method
100                                 */
101        dontEncode.set('-');
102        dontEncode.set('_');
103        dontEncode.set('.');
104        dontEncode.set('*');
105
106        String loc = SystemPropertiesManager.get(Constants.AM_LOCALE, "en_US");
107        defaultLocale = getLocale(loc);
108        normalizedDateFormat = new SimpleDateFormat(normalizedDateString);
109    }
110
111    public static void main(String[] args) {
112        System.out.println(":" + Locale.getLocale(args[0]) + ":");
113        System.out.println(":" + Locale.getLocale(args[0]).getCountry() + ":");
114    }
115
116    /**
117     * Gets the locale object for the specified localized string format.
118     * 
119     * @param stringformat
120     *            String representation of the locale. Examples:
121     *            <code>en_US, en_UK, ja_JP</code>.
122     * @return the <code>java.util.locale</code> object.
123     */
124    public static java.util.Locale getLocale(String stringformat) {
125        java.util.Locale locale = java.util.Locale.getDefault();
126        if (stringformat == null) {
127            return locale;
128        }
129
130        StringTokenizer tk = null;
131        String lang = "";
132        String country = "";
133        String variant = "";
134
135        if (stringformat.indexOf(HYPHEN) != -1) {
136            tk = new StringTokenizer(stringformat,HYPHEN);
137        } else {
138            tk = new StringTokenizer(stringformat,UNDERSCORE);
139        }
140
141        if (tk != null) {
142            if (tk.hasMoreTokens()) {
143                lang = tk.nextToken();
144            }
145            if (tk.hasMoreTokens()) {
146                country = tk.nextToken();
147            }
148            if (tk.hasMoreTokens()) {
149                variant = tk.nextToken();
150            }
151            locale = new java.util.Locale(lang, country, variant);
152        } 
153
154        return locale;
155    }
156
157    /**
158     * Returns locale from accept-language header HTTP accept language header
159     * can have more than one language in the header, we honor the first
160     * language as locale
161     * 
162     * @param langstr
163     *            Value from Accept-Language header of HTTP
164     * @return locale string in this format <code>en_US, fr</code>
165     */
166    public static String getLocaleStringFromAcceptLangHeader(String langstr) {
167
168        if (langstr == null)
169            return null;
170
171        char[] lstr = langstr.toCharArray();
172        int leadSpace = 0;
173        /*
174         * Accept Language Syntax Accept-Language = "Accept-Language" ":" 1#(
175         * language-range [ ";" "q" "=" qvalue ] ) language-range = ( ( 1*8ALPHA
176         * *("-" 1*8ALPHA ) ) | "*" ) For more info Read RFC 2616 Examples:
177         * Accept-Language: da, en-gb;q=0.8, en;q=0.7 Accept-Language: en-gb, en
178         * Accept-Language: ja Accept-Language: zh-cn Accept-Language: *
179         * 
180         * We will use first language as locale. We will not process any
181         * further.Netscape,IE will give mostly one language as Accept-Language
182         * header. Max length of string is 5 lang-> 2chars , country -> two
183         * chars and separator is -
184         */
185
186        try {
187            while (Character.isWhitespace(lstr[leadSpace]))
188                leadSpace++;
189            int len = lstr.length;
190            if (len > leadSpace + LOCALE_STRING_MAX_LEN)
191                len = leadSpace + LOCALE_STRING_MAX_LEN;
192
193            boolean isCountry = false;
194            for (int i = leadSpace; i < len; i++) {
195                char ch = lstr[i];
196                if (ch == '*')
197                    return null;
198                // "*" can be a valid accept-lang but does
199                // give idea about locale, return null and force the caller to
200                // use
201                // default locale
202                if (ch == '-') {
203                    lstr[i] = '_'; // We will follow Java mechanism en_US
204                    isCountry = true;
205                } else if (ch == ';' || ch == ',') {// Language separators used
206                                                    // by accept-lang
207                    return new String(lstr, leadSpace, i - leadSpace);
208                } else if (isCountry) {
209                    lstr[i] = Character.toUpperCase(ch);
210                }
211            }
212            return new String(lstr, 0, len);
213        } catch (IndexOutOfBoundsException ex) {
214            return null;
215        }
216    }
217
218    /**
219     * Gets locale from accept-language header HTTP accept language header can
220     * have more than one language in the header, we honor the first language as
221     * locale
222     * 
223     * @param langStr
224     *            Value from Accept-Language header of HTTP
225     * @return locale string in this format <code>en_US, fr</code>.
226     */
227    public static java.util.Locale getLocaleObjFromAcceptLangHeader(
228            String langStr) {
229        String lstr = getLocaleStringFromAcceptLangHeader(langStr);
230
231        if (lstr == null)
232            return null;
233        String lang = lstr.substring(0, 2);
234        String country = "";
235        if (lstr.length() == LOCALE_STRING_MAX_LEN)
236            country = lstr.substring(3, 5);
237        return new java.util.Locale(lang, country);
238    }
239
240    /**
241     * Gets the resource bundle corresponding to the specified locale and the
242     * localized property file name.
243     * 
244     * @param bundle
245     *            Localized property file name.
246     * @param stringformat
247     *            String representation of the locale.
248     * 
249     * @return <code>java.util.ResourceBundle</code> object.
250     * 
251     */
252    public static ResourceBundle getResourceBundle(String bundle,
253            String stringformat) {
254        return ResourceBundle.getBundle(bundle, getLocale(stringformat));
255    }
256
257    protected static ResourceBundle getResourceBundle(String bundle) {
258        return getInstallResourceBundle(bundle);
259    }
260
261    /**
262     * Gets the default install resource bundle for the default locale
263     * 
264     * @param bundle
265     *            Localized property file name
266     * @return the install resource bundle object
267     */
268    public static ResourceBundle getInstallResourceBundle(String bundle) {
269        String loc = SystemPropertiesManager.get(Constants.AM_LOCALE, "en_US");
270        return ResourceBundle.getBundle(bundle, getLocale(loc));
271    }
272
273    /**
274     * Gets the default locale
275     * 
276     * @return the default Locale object
277     */
278    public static java.util.Locale getDefaultLocale() {
279        String loc = SystemPropertiesManager.get(Constants.AM_LOCALE, "en_US");
280        return getLocale(loc);
281    }
282
283    /**
284     * Formats messages using <code>MessageFormat</code> Class.
285     * 
286     * @param formatStr
287     *            string format template.
288     * @param obj1
289     *            object to be added to the template.
290     * @return formatted message.
291     */
292    public static String formatMessage(String formatStr, Object obj1) {
293        Object arr[] = new Object[1];
294        arr[0] = obj1;
295        return MessageFormat.format(formatStr, arr);
296    }
297
298    /**
299     * Formats to format messages using <code>MessageFormat</code> Class.
300     * given params to format them with
301     * 
302     * @param formatStr
303     *            string format template.
304     * @param objs
305     *            objects to be added to the template.
306     * @return formatted message.
307     */
308    public static String formatMessage(String formatStr, Object[] objs) {
309        return MessageFormat.format(formatStr, objs);
310    }
311
312    /**
313     * Returns the Date object from the date string in <code>ISO-8601</code>
314     * format. OpenAM stores date in <code>ISO-8601</code> format
315     * <code>yyyy/MM/yy hh:mm</code>
316     * 
317     * @param dateString
318     *            in the format <code>2002/12/31 23:59</code>.
319     * @return Date object
320     */
321    public static Date parseNormalizedDateString(String dateString) {
322        if (dateString == null)
323            return null;
324
325        ParsePosition pos = new ParsePosition(0);
326        Date date = normalizedDateFormat.parse(dateString, pos);
327        if (date == null) {
328            debug.error("Locale.parseNormalizedDateString: "
329                    + "Unable to parse date string");
330        }
331        if (debug.messageEnabled()) {
332            debug.message("Locale.parseNormalizedDateString(" + dateString
333                    + ")=" + date);
334        }
335        return date;
336
337    }
338
339    /**
340     * Gets Date object from date string with specified locale.
341     * 
342     * @param dateString
343     *            date string
344     * @param locale
345     *            Locale object
346     * @param dateSyntax
347     *            syntax of the date string.
348     * 
349     * @return Date object returned if <code>dateString</code> matches the
350     *         <code> dateSyntax</code>. If the syntax or date string is
351     *         empty, or the string does not match the syntax, null will be
352     *         returned.
353     */
354    public static Date parseDateString(String dateString,
355            java.util.Locale locale, String dateSyntax) {
356        if (debug.messageEnabled()) {
357            debug.message("Local.parseDateString(date, locale, syntax)");
358            debug.message("date string = " + dateString);
359            debug.message("date syntax = " + dateSyntax);
360            debug.message("locale = " + locale.toString());
361        }
362        if ((dateString == null) || (dateString.length() < 1)
363                || (dateSyntax == null) || (dateSyntax.length() < 1)) {
364            return null;
365        }
366
367        SimpleDateFormat sdf = new SimpleDateFormat(dateSyntax);
368        sdf.setLenient(false);
369        ParsePosition pos = new ParsePosition(0);
370        Date date = sdf.parse(dateString, pos);
371        if (date == null) {
372            debug.warning("Locale.parseDateString: unable to parse the date.");
373        }
374
375        return date;
376    }
377
378    /**
379     * Gets Date object from date string with specified locale. Syntax of date
380     * string is defined in amUser_&lt;locale> properties file.
381     * 
382     * @param dateString
383     *            date string
384     * @param locale
385     *            Locale object
386     * 
387     * @return Date object. null will be returned if error happens
388     */
389    public static Date parseDateString(String dateString,
390            java.util.Locale locale) {
391        ResourceBundle rb = AMResourceBundleCache.getInstance().getResBundle(
392                USER_PROPERTIES, locale);
393
394        if (rb == null) {
395            debug.error("Locale.parseDateString: Unable to get resource "
396                    + "bundle. Locale = " + locale);
397            return null;
398        }
399
400        String dateSyntax = null;
401        try {
402            dateSyntax = rb.getString(DATE_SYNTAX);
403            dateSyntax = dateSyntax.trim();
404        } catch (Exception ex) {
405            debug.error("Locale.parseDateString: Unable to get " + DATE_SYNTAX
406                    + ". Locale " + locale);
407            return null;
408        }
409        return parseDateString(dateString, locale, dateSyntax);
410    }
411
412    /**
413     * Converts the Date object into <code>ISO-8601</code> format
414     * <code>yyyy/MM/dd HH:mm</code> like <code>2002/12/23 20:40</code>.
415     * 
416     * @param date
417     *            to be normalized.
418     * @return date in <code>ISO8601</code> format
419     *         <code>2002/12/31 11:59</code>.
420     */
421    public static String getNormalizedDateString(Date date) {
422        if (date == null)
423            return null;
424        return normalizedDateFormat.format(date);
425    }
426
427    /**
428     * Gets date string from date with specified locale.
429     * 
430     * @param date
431     *            Date object
432     * @param locale
433     *            Locale object
434     * 
435     * @return date string. null will be returned if error happens
436     */
437    public static String getDateString(Date date, java.util.Locale locale) {
438        if (date == null) {
439            return null;
440        }
441
442        ResourceBundle rb = AMResourceBundleCache.getInstance().getResBundle(
443                USER_PROPERTIES, locale);
444        if (rb == null) {
445            debug.error("Locale.getDateString: Unable to get resource "
446                    + "bundle. Locale = " + locale);
447            return null;
448        }
449
450        String dateSyntax = null;
451        try {
452            dateSyntax = rb.getString(DATE_SYNTAX);
453        } catch (Exception ex) {
454            debug.error("Locale.getDateString: Unable to get " + DATE_SYNTAX
455                    + ". Locale " + locale);
456            return null;
457        }
458
459        if (debug.messageEnabled()) {
460            debug.message("Locale.getDateString: dateSyntax = " + dateSyntax);
461        }
462
463        SimpleDateFormat sdf = new SimpleDateFormat(dateSyntax);
464        return sdf.format(date);
465    }
466
467    /**
468     * Converts date string from source locale to destination locale
469     * 
470     * @param srcDateString
471     *            source date string
472     * @param srcLocale
473     *            source Locale object
474     * @param dstLocale
475     *            destination Locale object
476     * 
477     * @return converted date string. null will be returned if error happens
478     */
479    public static String convertDateString(String srcDateString,
480            java.util.Locale srcLocale, java.util.Locale dstLocale) {
481        Date date = parseDateString(srcDateString, srcLocale);
482
483        return getDateString(date, dstLocale);
484    }
485
486    /**
487     * Gets the localized string for the specified key formatted as per passed
488     * parameters.
489     * 
490     * @param rb
491     *            resource bundle.
492     * @param resource
493     *            the specified key.
494     * @param params
495     *            formatting done as per these parameters.
496     * 
497     * @return the localized string representation formatted as per passed
498     *         parameters.
499     */
500    public static String getString(ResourceBundle rb, String resource,
501            Object[] params) {
502        try {
503            return MessageFormat.format(rb.getString(resource), params);
504        } catch (Exception mre) {
505            if (debug.messageEnabled()) {
506                debug.message("missing resource: " + resource);
507            }
508        }
509        return resource;
510    }
511
512    /**
513     * Gets the localized string for the specified key from the specified
514     * Resource or from the specified default resource formatted as per provided
515     * parameters.
516     * 
517     * @param rb
518     *            resource bundle.
519     * @param resource
520     *            the specified key.
521     * @param defaultRb
522     *            Default resource bundle.
523     * @param params
524     *            formatting done as per these parameters.
525     * 
526     * @return the localized string representation formatted as per passed
527     *         parameters.
528     * 
529     */
530    public static String getString(ResourceBundle rb, String resource,
531            ResourceBundle defaultRb, Object[] params) {
532        try {
533            return MessageFormat.format(rb.getString(resource), params);
534        } catch (Exception mre) {
535            try {
536                if (debug.messageEnabled()) {
537                    debug.message("missing resource: " + resource);
538                    debug.message("fall back to default resource bundle");
539                }
540                return MessageFormat.format(defaultRb.getString(resource),
541                        params);
542            } catch (Exception mrde) {
543                if (debug.messageEnabled()) {
544                    debug.message("missing resource in default resource bundle:"
545                                    + resource);
546                }
547            }
548        }
549        return resource;
550    }
551
552    /**
553     * Gets the localized string for the specified key
554     * 
555     * @param rb
556     *            resource bundle.
557     * @param resource
558     *            the specified key.
559     * @param debug
560     *            the debug instance to which the debug messages need to be
561     *            printed.
562     * 
563     * @return the localized string representation
564     */
565    public static String getString(ResourceBundle rb, String resource,
566            Debug debug) {
567        try {
568            return rb.getString(resource);
569        } catch (Exception mre) {
570            if (debug.messageEnabled()) {
571                debug.message("missing resource: " + resource);
572            }
573        }
574        return resource;
575    }
576
577    /**
578     * Gets the localized string for the specified key from the specified
579     * Resource or from the specified default resource
580     * 
581     * @param rb
582     *            resource bundle.
583     * @param resource
584     *            the specified key.
585     * @param debug
586     *            the debug instance to which the debug messages need to be
587     *            printed.
588     * @param defaultRb
589     *            Default resource bundle.
590     * 
591     * @return the localized string representation
592     */
593    public static String getString(ResourceBundle rb, String resource,
594            Debug debug, ResourceBundle defaultRb) {
595        try {
596            return rb.getString(resource);
597        } catch (Exception mre) {
598            try {
599                if (debug.messageEnabled()) {
600                    debug.message("missing resource: " + resource);
601                    debug.message("fall back to default resource bundle");
602                }
603                return defaultRb.getString(resource);
604            } catch (Exception mrde) {
605                if (debug.messageEnabled()) {
606                    debug.message("missing resource in default resource bundle:"
607                                    + resource);
608                }
609            }
610        }
611        return resource;
612    }
613
614    /**
615     * Gets the localized string for the specified key.
616     * 
617     * @param rb
618     *            resource bundle.
619     * @param resource
620     *            the specified key.
621     * @return the localized string representation
622     */
623    public static String getString(ResourceBundle rb, String resource) {
624        try {
625            return rb.getString(resource);
626        } catch (Exception mre) {
627            if (debug.messageEnabled()) {
628                debug.message("missing resource: " + resource);
629            }
630        }
631        return resource;
632    }
633
634    /**
635     * Gets the localized string for the specified key from the specified
636     * Resource or from the specified default resource.
637     * 
638     * @param rb
639     *            resource bundle.
640     * @param resource
641     *            the specified key.
642     * @param defaultRb
643     *            Default resource bundle.
644     * @return the localized string representation
645     */
646    public static String getString(ResourceBundle rb, String resource,
647            ResourceBundle defaultRb) {
648        try {
649            return rb.getString(resource);
650        } catch (Exception mre) {
651            try {
652                if (debug.messageEnabled()) {
653                    debug.message("missing resource: " + resource);
654                    debug.message("fall back to default resource bundle");
655                }
656                return defaultRb.getString(resource);
657            } catch (Exception mrde) {
658                if (debug.messageEnabled()) {
659                    debug.message("missing resource in default resource bundle:"
660                                    + resource);
661                }
662            }
663        }
664        return resource;
665    }
666
667    /**
668     * This method is replacement function for <code>URLEncoder</code>
669     * Function URL encoder function converts input string into
670     * <code>URLEncoded</code> byte stream after converting Unicode string
671     * into bytes using native encoding. The <code>URLEncoder</code> does not
672     * work for OpenAM if default encoding is not
673     * <code>UTF-8</code>, hence this method was written.
674     * 
675     * @param input
676     *            the input string.
677     * @param enc
678     *            the encoding format.
679     * @return the encoded string.
680     * @throws UnsupportedEncodingException
681     */
682    public static String URLEncodeField(String input, String enc)
683            throws UnsupportedEncodingException {
684        int inputLen = input.length();
685
686        byte[] byteOut = input.getBytes(enc);
687        StringBuffer result = new StringBuffer(inputLen * 4); // approx size
688        for (int i = 0; i < byteOut.length; i++) {
689            int c = byteOut[i] & 0xff;
690            if (dontEncode.get(c)) {
691                if (c == ' ') {
692                    c = '+';
693                }
694                result.append((char) c);
695            } else {
696                result.append('%');
697                char ch = Character.forDigit((c >> 4) & 0xF, 16);
698                if (('a' <= ch) && (ch <= 'f')) {
699                    ch -= caseDiff;
700                }
701                result.append(ch);
702                ch = Character.forDigit(c & 0xF, 16);
703                if (('a' <= ch) && (ch <= 'f')) {
704                    ch -= caseDiff;
705                }
706                result.append(ch);
707            }
708
709        }
710        return result.toString();
711    }
712
713    /**
714     * This method is replacement function for <code>URLEncoder<code> Function
715     * URL encoder function converts input string into <code>URLencoded</code>
716     * byte stream after converting Unicode string into bytes using native
717     * encoding. The <code>URLEncoder</code> does not work for Sun Java System
718     * OpenAM if default encoding is not <code>UTF-8</code>, hence this
719     * method was written.
720     * 
721     * @param input the input string
722     * @param enc the encoding format 
723     * @param debug the debug instance to which debug messages need to
724     * be printed
725     *
726     * @return the encoded string
727     */
728    public static String URLEncodeField(String input, String enc, Debug debug) {
729        int inputLen = input.length();
730
731        byte[] byteOut;
732        try {
733            byteOut = input.getBytes(enc);
734        } catch (UnsupportedEncodingException ex) {
735            if (debug != null) {
736                debug.error("Locale.URLEncodeField: Unsupported Encoding "
737                        + enc, ex);
738            }
739            return input;
740        }
741
742        StringBuffer result = new StringBuffer(inputLen * 4); // approx size
743        for (int i = 0; i < byteOut.length; i++) {
744            int c = byteOut[i] & 0xff;
745            if (dontEncode.get(c)) {
746                if (c == ' ') {
747                    c = '+';
748                }
749                result.append((char) c);
750            } else {
751                result.append('%');
752                char ch = Character.forDigit((c >> 4) & 0xF, 16);
753                if (('a' <= ch) && (ch <= 'f')) {
754                    ch -= caseDiff;
755                }
756                result.append(ch);
757                ch = Character.forDigit(c & 0xF, 16);
758                if (('a' <= ch) && (ch <= 'f')) {
759                    ch -= caseDiff;
760                }
761                result.append(ch);
762            }
763
764        }
765        return result.toString();
766    }
767
768    static public String URLDecodeField(String strIn, Debug debug) {
769        return URLDecodeField(strIn, "UTF-8", debug);
770    }
771
772    /*
773     * Translate the individual field values in the encoding value Do not use
774     * getBytes instead convert unicode into bytes by casting. Using getBytes
775     * results in conversion into platform encoding. It appears to work file in
776     * C locale because default encoding is 8859-1 but fails in japanese locale.
777     * 
778     * @param strIn the inputString @param charset character encoding of
779     * inputString @param debug the debug instance to which debug messages need
780     * to be printed.
781     * 
782     * @return the decoded string
783     */
784    static public String URLDecodeField(String strIn, String charset,
785            Debug debug) {
786
787        if (strIn == null) {
788            return strIn;
789        }
790        String strOut = null;
791        try {
792            int len = strIn.length();
793            byte buf[] = new byte[len];
794
795            int i = 0;
796            int offset = 0;
797            char[] carr = strIn.toCharArray();
798            while (i < len) {
799                byte b = (byte) carr[i];
800                switch (b) {
801                case '%':
802                    int val = 0;
803                    if (i + 2 < len) {
804                        i++;
805                        b = (byte) carr[i];
806                        if ('a' <= b && b <= 'f') {
807                            b -= caseDiff;
808                        }
809                        if ('A' <= b && b <= 'F') {
810                            val = 10 + b - 'A';
811                            val = val << 4;
812                        } else if ('0' <= b && b <= '9') {
813                            val = (b - '0') << 4;
814                        } else {
815                            throw new IllegalArgumentException(
816                                    "invalid hex char");
817                        }
818                        i++;
819                        b = (byte) carr[i];
820                        if ('a' <= b && b <= 'f') {
821                            b -= caseDiff;
822                        }
823                        if ('A' <= b && b <= 'F') {
824                            val += 10 + b - 'A';
825                        } else if ('0' <= b && b <= '9') {
826                            val += b - '0';
827                        } else {
828                            throw new IllegalArgumentException(
829                                    "invalid hex char");
830                        }
831                        buf[offset++] = (byte) val;
832                        i++;
833                    } else {
834                       buf[offset++] = (byte) carr[i++]; 
835                    }
836                    break;
837                default:
838                    buf[offset++] = (byte) carr[i++];
839                    break;
840                }
841            }
842            if (charset == null || charset.length() == 0) {
843                strOut = new String(buf, 0, offset, "UTF-8");
844            } else {
845                strOut = new String(buf, 0, offset, charset);
846            }
847        } catch (Exception ex) {
848            debug.error("Locale::decodeField", ex);
849            strOut = strIn;
850        }
851        return strOut;
852    }
853}