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 " 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 " 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. '&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 "<"
359 * <li> '>' to ">"
360 * <li> '&' to "&"
361 * <li> '"' to """
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 "<".
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 "<".
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 "<".
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 "<".
649 * @return true if the mapping is not one of:
650 * <ul>
651 * <li> '<' to "<"
652 * <li> '>' to ">"
653 * <li> '&' to "&"
654 * <li> '"' to """
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("""))
666 extra = true;
667 break;
668 case '&' : // amp
669 if (!outputString.equals("&"))
670 extra = true;
671 break;
672 case '<' : // lt
673 if (!outputString.equals("<"))
674 extra = true;
675 break;
676 case '>' : // gt
677 if (!outputString.equals(">"))
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 "<".
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 "<"
724 * <li> '>' to ">"
725 * <li> '&' to "&"
726 * <li> '"' to """
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 }