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