001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements. See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership. The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the  "License");
007     * you may not use this file except in compliance with the License.
008     * You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    /*
019     * $Id: CharInfo.java 468654 2006-10-28 07:09:23Z minchau $
020     */
021    package org.apache.xml.serializer;
022    
023    import java.io.BufferedReader;
024    import java.io.InputStream;
025    import java.io.InputStreamReader;
026    import java.io.UnsupportedEncodingException;
027    import java.net.URL;
028    import java.util.Enumeration;
029    import java.util.HashMap;
030    import java.util.Hashtable;
031    import java.util.PropertyResourceBundle;
032    import java.util.ResourceBundle;
033    import java.security.AccessController;
034    import java.security.PrivilegedAction;
035    
036    import javax.xml.transform.TransformerException;
037    
038    import org.apache.xml.serializer.utils.MsgKey;
039    import org.apache.xml.serializer.utils.SystemIDResolver;
040    import org.apache.xml.serializer.utils.Utils;
041    import org.apache.xml.serializer.utils.WrappedRuntimeException;
042    
043    /**
044     * This class provides services that tell if a character should have
045     * special treatement, such as entity reference substitution or normalization
046     * of a newline character.  It also provides character to entity reference
047     * lookup.
048     *
049     * DEVELOPERS: See Known Issue in the constructor.
050     * 
051     * @xsl.usage internal
052     */
053    final class CharInfo
054    {
055        /** Given a character, lookup a String to output (e.g. a decorated entity reference). */
056        private HashMap m_charToString;
057    
058        /**
059         * The name of the HTML entities file.
060         * If specified, the file will be resource loaded with the default class loader.
061         */
062        public static final String HTML_ENTITIES_RESOURCE = 
063                    SerializerBase.PKG_NAME+".HTMLEntities";
064    
065        /**
066         * The name of the XML entities file.
067         * If specified, the file will be resource loaded with the default class loader.
068         */
069        public static final String XML_ENTITIES_RESOURCE = 
070                    SerializerBase.PKG_NAME+".XMLEntities";
071    
072        /** The horizontal tab character, which the parser should always normalize. */
073        static final char S_HORIZONAL_TAB = 0x09;
074    
075        /** The linefeed character, which the parser should always normalize. */
076        static final char S_LINEFEED = 0x0A;
077    
078        /** The carriage return character, which the parser should always normalize. */
079        static final char S_CARRIAGERETURN = 0x0D;
080        static final char S_SPACE = 0x20;
081        static final char S_QUOTE = 0x22;
082        static final char S_LT = 0x3C;
083        static final char S_GT = 0x3E;
084        static final char S_NEL = 0x85;    
085        static final char S_LINE_SEPARATOR = 0x2028;
086        
087        /** This flag is an optimization for HTML entities. It false if entities 
088         * other than quot (34), amp (38), lt (60) and gt (62) are defined
089         * in the range 0 to 127.
090         * @xsl.usage internal
091         */    
092        boolean onlyQuotAmpLtGt;
093        
094        /** Copy the first 0,1 ... ASCII_MAX values into an array */
095        static final int ASCII_MAX = 128;
096        
097        /** Array of values is faster access than a set of bits 
098         * to quickly check ASCII characters in attribute values,
099         * the value is true if the character in an attribute value
100         * should be mapped to a String. 
101         */
102        private final boolean[] shouldMapAttrChar_ASCII;
103        
104        /** Array of values is faster access than a set of bits 
105         * to quickly check ASCII characters in text nodes, 
106         * the value is true if the character in a text node
107         * should be mapped to a String. 
108         */
109        private final boolean[] shouldMapTextChar_ASCII;
110    
111        /** An array of bits to record if the character is in the set.
112         * Although information in this array is complete, the
113         * isSpecialAttrASCII array is used first because access to its values
114         * is common and faster.
115         */   
116        private final int array_of_bits[];
117         
118        
119        // 5 for 32 bit words,  6 for 64 bit words ...
120        /*
121         * This constant is used to shift an integer to quickly
122         * calculate which element its bit is stored in.
123         * 5 for 32 bit words (int) ,  6 for 64 bit words (long)
124         */
125        private static final int SHIFT_PER_WORD = 5;
126        
127        /*
128         * A mask to get the low order bits which are used to
129         * calculate the value of the bit within a given word,
130         * that will represent the presence of the integer in the 
131         * set.
132         * 
133         * 0x1F for 32 bit words (int),
134         * or 0x3F for 64 bit words (long) 
135         */
136        private static final int LOW_ORDER_BITMASK = 0x1f;
137        
138        /*
139         * This is used for optimizing the lookup of bits representing
140         * the integers in the set. It is the index of the first element
141         * in the array array_of_bits[] that is not used.
142         */
143        private int firstWordNotUsed;
144    
145    
146        /**
147         * A base constructor just to explicitly create the fields,
148         * with the exception of m_charToString which is handled
149         * by the constructor that delegates base construction to this one.
150         * <p>
151         * m_charToString is not created here only for performance reasons,
152         * to avoid creating a Hashtable that will be replaced when
153         * making a mutable copy, {@link #mutableCopyOf(CharInfo)}. 
154         *
155         */
156        private CharInfo() 
157        {
158            this.array_of_bits = createEmptySetOfIntegers(65535);
159            this.firstWordNotUsed = 0;
160            this.shouldMapAttrChar_ASCII = new boolean[ASCII_MAX];
161            this.shouldMapTextChar_ASCII = new boolean[ASCII_MAX];
162            this.m_charKey = new CharKey();
163            
164            // Not set here, but in a constructor that uses this one
165            // this.m_charToString =  new Hashtable();  
166            
167            this.onlyQuotAmpLtGt = true;
168            
169    
170            return;
171        }
172        
173        private CharInfo(String entitiesResource, String method, boolean internal)
174        {
175            // call the default constructor to create the fields
176            this();
177            m_charToString = new HashMap();
178    
179            ResourceBundle entities = null;
180            boolean noExtraEntities = true;
181    
182            // Make various attempts to interpret the parameter as a properties
183            // file or resource file, as follows:
184            //
185            //   1) attempt to load .properties file using ResourceBundle
186            //   2) try using the class loader to find the specified file a resource
187            //      file
188            //   3) try treating the resource a URI
189    
190            if (internal) { 
191                try {
192                    // Load entity property files by using PropertyResourceBundle,
193                    // cause of security issure for applets
194                    entities = PropertyResourceBundle.getBundle(entitiesResource);
195                } catch (Exception e) {}
196            }
197    
198            if (entities != null) {
199                Enumeration keys = entities.getKeys();
200                while (keys.hasMoreElements()){
201                    String name = (String) keys.nextElement();
202                    String value = entities.getString(name);
203                    int code = Integer.parseInt(value);
204                    boolean extra = defineEntity(name, (char) code);
205                    if (extra)
206                        noExtraEntities = false;
207                }
208            } else {
209                InputStream is = null;
210    
211                // Load user specified resource file by using URL loading, it
212                // requires a valid URI as parameter
213                try {
214                    if (internal) {
215                        is = CharInfo.class.getResourceAsStream(entitiesResource);
216                    } else {
217                        ClassLoader cl = ObjectFactory.findClassLoader();
218                        if (cl == null) {
219                            is = ClassLoader.getSystemResourceAsStream(entitiesResource);
220                        } else {
221                            is = cl.getResourceAsStream(entitiesResource);
222                        }
223    
224                        if (is == null) {
225                            try {
226                                URL url = new URL(entitiesResource);
227                                is = url.openStream();
228                            } catch (Exception e) {}
229                        }
230                    }
231    
232                    if (is == null) {
233                        throw new RuntimeException(
234                            Utils.messages.createMessage(
235                                MsgKey.ER_RESOURCE_COULD_NOT_FIND,
236                                new Object[] {entitiesResource, entitiesResource}));
237                    }
238    
239                    // Fix Bugzilla#4000: force reading in UTF-8
240                    //  This creates the de facto standard that Xalan's resource 
241                    //  files must be encoded in UTF-8. This should work in all
242                    // JVMs.
243                    //
244                    // %REVIEW% KNOWN ISSUE: IT FAILS IN MICROSOFT VJ++, which
245                    // didn't implement the UTF-8 encoding. Theoretically, we should
246                    // simply let it fail in that case, since the JVM is obviously
247                    // broken if it doesn't support such a basic standard.  But
248                    // since there are still some users attempting to use VJ++ for
249                    // development, we have dropped in a fallback which makes a
250                    // second attempt using the platform's default encoding. In VJ++
251                    // this is apparently ASCII, which is subset of UTF-8... and
252                    // since the strings we'll be reading here are also primarily
253                    // limited to the 7-bit ASCII range (at least, in English
254                    // versions of Xalan), this should work well enough to keep us
255                    // on the air until we're ready to officially decommit from
256                    // VJ++.
257    
258                    BufferedReader reader;
259                    try {
260                        reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
261                    } catch (UnsupportedEncodingException e) {
262                        reader = new BufferedReader(new InputStreamReader(is));
263                    }
264    
265                    String line = reader.readLine();
266    
267                    while (line != null) {
268                        if (line.length() == 0 || line.charAt(0) == '#') {
269                            line = reader.readLine();
270    
271                            continue;
272                        }
273    
274                        int index = line.indexOf(' ');
275    
276                        if (index > 1) {
277                            String name = line.substring(0, index);
278    
279                            ++index;
280    
281                            if (index < line.length()) {
282                                String value = line.substring(index);
283                                index = value.indexOf(' ');
284    
285                                if (index > 0) {
286                                    value = value.substring(0, index);
287                                }
288    
289                                int code = Integer.parseInt(value);
290    
291                                boolean extra = defineEntity(name, (char) code);
292                                if (extra)
293                                    noExtraEntities = false;
294                            }
295                        }
296    
297                        line = reader.readLine();
298                    }
299    
300                    is.close();
301                } catch (Exception e) {
302                    throw new RuntimeException(
303                        Utils.messages.createMessage(
304                            MsgKey.ER_RESOURCE_COULD_NOT_LOAD,
305                            new Object[] { entitiesResource,
306                                           e.toString(),
307                                           entitiesResource,
308                                           e.toString()}));
309                } finally {
310                    if (is != null) {
311                        try {
312                            is.close();
313                        } catch (Exception except) {}
314                    }
315                }
316            }
317    
318            onlyQuotAmpLtGt = noExtraEntities;
319                
320            /* Now that we've used get(ch) just above to initialize the
321             * two arrays we will change by adding a tab to the set of 
322             * special chars for XML (but not HTML!).
323             * We do this because a tab is always a
324             * special character in an XML attribute, 
325             * but only a special character in XML text 
326             * if it has an entity defined for it.
327             * This is the reason for this delay.
328             */
329            if (Method.XML.equals(method)) 
330            {       
331                // We choose not to escape the quotation mark as &quot; in text nodes
332                shouldMapTextChar_ASCII[S_QUOTE] = false;
333            }
334            
335            if (Method.HTML.equals(method)) {
336                    // The XSLT 1.0 recommendation says 
337                    // "The html output method should not escape < characters occurring in attribute values."
338                    // So we don't escape '<' in an attribute for HTML
339                    shouldMapAttrChar_ASCII['<'] = false;    
340                    
341                    // We choose not to escape the quotation mark as &quot; in text nodes.
342                shouldMapTextChar_ASCII[S_QUOTE] = false;
343            }
344        }
345    
346        /**
347         * Defines a new character reference. The reference's name and value are
348         * supplied. Nothing happens if the character reference is already defined.
349         * <p>Unlike internal entities, character references are a string to single
350         * character mapping. They are used to map non-ASCII characters both on
351         * parsing and printing, primarily for HTML documents. '&amp;lt;' is an
352         * example of a character reference.</p>
353         *
354         * @param name The entity's name
355         * @param value The entity's value
356         * @return true if the mapping is not one of:
357         * <ul>
358         * <li> '<' to "&lt;"
359         * <li> '>' to "&gt;"
360         * <li> '&' to "&amp;"
361         * <li> '"' to "&quot;"
362         * </ul>
363         */
364        private boolean defineEntity(String name, char value)
365        {
366            StringBuffer sb = new StringBuffer("&");
367            sb.append(name);
368            sb.append(';');
369            String entityString = sb.toString();
370            
371            boolean extra = defineChar2StringMapping(entityString, value);
372            return extra;
373        }
374    
375        /**
376         * A utility object, just used to map characters to output Strings,
377         * needed because a HashMap needs to map an object as a key, not a 
378         * Java primitive type, like a char, so this object gets around that
379         * and it is reusable.
380         */
381        private final CharKey m_charKey;
382    
383        /**
384         * Map a character to a String. For example given
385         * the character '>' this method would return the fully decorated
386         * entity name "&lt;".
387         * Strings for entity references are loaded from a properties file,
388         * but additional mappings defined through calls to defineChar2String()
389         * are possible. Such entity reference mappings could be over-ridden.
390         *
391         * This is reusing a stored key object, in an effort to avoid
392         * heap activity. Unfortunately, that introduces a threading risk.
393         * Simplest fix for now is to make it a synchronized method, or to give
394         * up the reuse; I see very little performance difference between them.
395         * Long-term solution would be to replace the hashtable with a sparse array
396         * keyed directly from the character's integer value; see DTM's
397         * string pool for a related solution.
398         *
399         * @param value The character that should be resolved to
400         * a String, e.g. resolve '>' to  "&lt;".
401         *
402         * @return The String that the character is mapped to, or null if not found.
403         * @xsl.usage internal
404         */
405        String getOutputStringForChar(char value)
406        {
407            // CharKey m_charKey = new CharKey(); //Alternative to synchronized
408            m_charKey.setChar(value);
409            return (String) m_charToString.get(m_charKey);
410        }
411        
412        /**
413         * Tell if the character argument that is from
414         * an attribute value has a mapping to a String.
415         * 
416         * @param value the value of a character that is in an attribute value
417         * @return true if the character should have any special treatment, 
418         * such as when writing out entity references.
419         * @xsl.usage internal
420         */
421        final boolean shouldMapAttrChar(int value)
422        {
423            // for performance try the values in the boolean array first,
424            // this is faster access than the BitSet for common ASCII values
425    
426            if (value < ASCII_MAX)
427                return shouldMapAttrChar_ASCII[value];
428    
429            // rather than java.util.BitSet, our private
430            // implementation is faster (and less general).
431            return get(value);
432        }    
433    
434        /**
435         * Tell if the character argument that is from a 
436         * text node has a mapping to a String, for example
437         * to map '<' to "&lt;".
438         * 
439         * @param value the value of a character that is in a text node
440         * @return true if the character has a mapping to a String, 
441         * such as when writing out entity references.
442         * @xsl.usage internal
443         */
444        final boolean shouldMapTextChar(int value)
445        {
446            // for performance try the values in the boolean array first,
447            // this is faster access than the BitSet for common ASCII values
448    
449            if (value < ASCII_MAX)
450                return shouldMapTextChar_ASCII[value];
451    
452            // rather than java.util.BitSet, our private
453            // implementation is faster (and less general).
454            return get(value);
455        }
456        
457    
458         
459        private static CharInfo getCharInfoBasedOnPrivilege(
460            final String entitiesFileName, final String method, 
461            final boolean internal){
462                return (CharInfo) AccessController.doPrivileged(
463                    new PrivilegedAction() {
464                            public Object run() {
465                                return new CharInfo(entitiesFileName, 
466                                  method, internal);}
467                });            
468        }
469         
470        /**
471         * Factory that reads in a resource file that describes the mapping of
472         * characters to entity references.
473         *
474         * Resource files must be encoded in UTF-8 and have a format like:
475         * <pre>
476         * # First char # is a comment
477         * Entity numericValue
478         * quot 34
479         * amp 38
480         * </pre>
481         * (Note: Why don't we just switch to .properties files? Oct-01 -sc)
482         *
483         * @param entitiesResource Name of entities resource file that should
484         * be loaded, which describes that mapping of characters to entity references.
485         * @param method the output method type, which should be one of "xml", "html", "text"...
486         * 
487         * @xsl.usage internal
488         */
489        static CharInfo getCharInfo(String entitiesFileName, String method)
490        {
491            CharInfo charInfo = (CharInfo) m_getCharInfoCache.get(entitiesFileName);
492            if (charInfo != null) {
493                    return mutableCopyOf(charInfo);
494            }
495    
496            // try to load it internally - cache
497            try {
498                charInfo = getCharInfoBasedOnPrivilege(entitiesFileName, 
499                                            method, true);
500                // Put the common copy of charInfo in the cache, but return
501                // a copy of it.
502                m_getCharInfoCache.put(entitiesFileName, charInfo);
503                return mutableCopyOf(charInfo);
504            } catch (Exception e) {}
505    
506            // try to load it externally - do not cache
507            try {
508                return getCharInfoBasedOnPrivilege(entitiesFileName, 
509                                    method, false);
510            } catch (Exception e) {}
511    
512            String absoluteEntitiesFileName;
513    
514            if (entitiesFileName.indexOf(':') < 0) {
515                absoluteEntitiesFileName =
516                    SystemIDResolver.getAbsoluteURIFromRelative(entitiesFileName);
517            } else {
518                try {
519                    absoluteEntitiesFileName =
520                        SystemIDResolver.getAbsoluteURI(entitiesFileName, null);
521                } catch (TransformerException te) {
522                    throw new WrappedRuntimeException(te);
523                }
524            }
525    
526            return getCharInfoBasedOnPrivilege(entitiesFileName, 
527                                    method, false);
528        }
529    
530        /**
531         * Create a mutable copy of the cached one.
532         * @param charInfo The cached one.
533         * @return
534         */
535        private static CharInfo mutableCopyOf(CharInfo charInfo) {
536            CharInfo copy = new CharInfo();
537            
538            int max = charInfo.array_of_bits.length;
539            System.arraycopy(charInfo.array_of_bits,0,copy.array_of_bits,0,max);
540            
541            copy.firstWordNotUsed = charInfo.firstWordNotUsed;
542            
543            max = charInfo.shouldMapAttrChar_ASCII.length;
544            System.arraycopy(charInfo.shouldMapAttrChar_ASCII,0,copy.shouldMapAttrChar_ASCII,0,max);
545            
546            max = charInfo.shouldMapTextChar_ASCII.length;
547            System.arraycopy(charInfo.shouldMapTextChar_ASCII,0,copy.shouldMapTextChar_ASCII,0,max);
548            
549            // utility field copy.m_charKey is already created in the default constructor 
550            
551            copy.m_charToString = (HashMap) charInfo.m_charToString.clone();
552            
553            copy.onlyQuotAmpLtGt = charInfo.onlyQuotAmpLtGt;
554                    
555                    return copy;
556            }
557    
558            /** 
559             * Table of user-specified char infos.
560             * The table maps entify file names (the name of the
561             * property file without the .properties extension)
562             * to CharInfo objects populated with entities defined in 
563             * corresponding property file.  
564             */
565        private static Hashtable m_getCharInfoCache = new Hashtable();
566    
567        /**
568         * Returns the array element holding the bit value for the
569         * given integer
570         * @param i the integer that might be in the set of integers
571         * 
572         */
573        private static int arrayIndex(int i) {
574            return (i >> SHIFT_PER_WORD);
575        }
576    
577        /**
578         * For a given integer in the set it returns the single bit
579         * value used within a given word that represents whether
580         * the integer is in the set or not.
581         */
582        private static int bit(int i) {
583            int ret = (1 << (i & LOW_ORDER_BITMASK));
584            return ret;
585        }
586    
587        /**
588         * Creates a new empty set of integers (characters)
589         * @param max the maximum integer to be in the set.
590         */
591        private int[] createEmptySetOfIntegers(int max) {
592            firstWordNotUsed = 0; // an optimization 
593    
594            int[] arr = new int[arrayIndex(max - 1) + 1];
595                return arr;
596     
597        }
598    
599        /**
600         * Adds the integer (character) to the set of integers.
601         * @param i the integer to add to the set, valid values are 
602         * 0, 1, 2 ... up to the maximum that was specified at
603         * the creation of the set.
604         */
605        private final void set(int i) {   
606            setASCIItextDirty(i);
607            setASCIIattrDirty(i); 
608                 
609            int j = (i >> SHIFT_PER_WORD); // this word is used
610            int k = j + 1;       
611            
612            if(firstWordNotUsed < k) // for optimization purposes.
613                firstWordNotUsed = k;
614                
615            array_of_bits[j] |= (1 << (i & LOW_ORDER_BITMASK));
616        }
617    
618    
619        /**
620         * Return true if the integer (character)is in the set of integers.
621         * 
622         * This implementation uses an array of integers with 32 bits per
623         * integer.  If a bit is set to 1 the corresponding integer is 
624         * in the set of integers.
625         * 
626         * @param i an integer that is tested to see if it is the
627         * set of integers, or not.
628         */
629        private final boolean get(int i) {
630    
631            boolean in_the_set = false;
632            int j = (i >> SHIFT_PER_WORD); // wordIndex(i)
633            // an optimization here, ... a quick test to see
634            // if this integer is beyond any of the words in use
635            if(j < firstWordNotUsed)
636                in_the_set = (array_of_bits[j] & 
637                              (1 << (i & LOW_ORDER_BITMASK))
638                ) != 0;  // 0L for 64 bit words
639            return in_the_set;
640        }
641        
642        /**
643         * This method returns true if there are some non-standard mappings to
644         * entities other than quot, amp, lt, gt, and its only purpose is for
645         * performance.
646         * @param charToMap The value of the character that is mapped to a String
647         * @param outputString The String to which the character is mapped, usually
648         * an entity reference such as "&lt;".
649         * @return true if the mapping is not one of:
650         * <ul>
651         * <li> '<' to "&lt;"
652         * <li> '>' to "&gt;"
653         * <li> '&' to "&amp;"
654         * <li> '"' to "&quot;"
655         * </ul>
656         */
657        private boolean extraEntity(String outputString, int charToMap)
658        {
659            boolean extra = false;
660            if (charToMap < ASCII_MAX)
661            {
662                switch (charToMap)
663                {
664                    case '"' : // quot
665                            if (!outputString.equals("&quot;"))
666                                    extra = true;  
667                            break;
668                    case '&' : // amp
669                            if (!outputString.equals("&amp;"))
670                                    extra = true;
671                            break;
672                    case '<' : // lt
673                            if (!outputString.equals("&lt;"))
674                                    extra = true;
675                            break;
676                    case '>' : // gt
677                            if (!outputString.equals("&gt;"))
678                                    extra = true;
679                        break;
680                    default : // other entity in range 0 to 127  
681                        extra = true;
682                }
683            }
684            return extra;
685        }    
686        
687        /**
688         * If the character is in the ASCII range then
689         * mark it as needing replacement with
690         * a String on output if it occurs in a text node.
691         * @param ch
692         */
693        private void setASCIItextDirty(int j) 
694        {
695            if (0 <= j && j < ASCII_MAX) 
696            {
697                shouldMapTextChar_ASCII[j] = true;
698            } 
699        }
700        
701        /**
702         * If the character is in the ASCII range then
703         * mark it as needing replacement with
704         * a String on output if it occurs in a attribute value.
705         * @param ch
706         */
707        private void setASCIIattrDirty(int j) 
708        {
709            if (0 <= j && j < ASCII_MAX) 
710            {
711                shouldMapAttrChar_ASCII[j] = true;
712            } 
713        }
714    
715        
716        /**
717         * Call this method to register a char to String mapping, for example
718         * to map '<' to "&lt;".
719         * @param outputString The String to map to.
720         * @param inputChar The char to map from.
721         * @return true if the mapping is not one of:
722         * <ul>
723         * <li> '<' to "&lt;"
724         * <li> '>' to "&gt;"
725         * <li> '&' to "&amp;"
726         * <li> '"' to "&quot;"
727         * </ul>
728         */
729        boolean defineChar2StringMapping(String outputString, char inputChar) 
730        {
731            CharKey character = new CharKey(inputChar);
732            m_charToString.put(character, outputString);
733            set(inputChar);  // mark the character has having a mapping to a String
734            
735            boolean extraMapping = extraEntity(outputString, inputChar);
736            return extraMapping;
737                    
738        }
739    
740        /**
741         * Simple class for fast lookup of char values, when used with
742         * hashtables.  You can set the char, then use it as a key.
743         *  
744         * @xsl.usage internal
745         */
746        private static class CharKey extends Object
747        {
748    
749          /** String value          */
750          private char m_char;
751    
752          /**
753           * Constructor CharKey
754           *
755           * @param key char value of this object.
756           */
757          public CharKey(char key)
758          {
759            m_char = key;
760          }
761      
762          /**
763           * Default constructor for a CharKey.
764           *
765           * @param key char value of this object.
766           */
767          public CharKey()
768          {
769          }
770      
771          /**
772           * Get the hash value of the character.  
773           *
774           * @return hash value of the character.
775           */
776          public final void setChar(char c)
777          {
778            m_char = c;
779          }
780    
781    
782    
783          /**
784           * Get the hash value of the character.  
785           *
786           * @return hash value of the character.
787           */
788          public final int hashCode()
789          {
790            return (int)m_char;
791          }
792    
793          /**
794           * Override of equals() for this object 
795           *
796           * @param obj to compare to
797           *
798           * @return True if this object equals this string value 
799           */
800          public final boolean equals(Object obj)
801          {
802            return ((CharKey)obj).m_char == m_char;
803          }
804        }
805       
806    
807    }