View Javadoc
1   /*
2   Copyright (c) 2008 Health Market Science, Inc.
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.InputStreamReader;
22  import java.util.Arrays;
23  import java.util.HashMap;
24  import java.util.Map;
25  
26  import static com.healthmarketscience.jackcess.impl.ByteUtil.ByteStream;
27  
28  /**
29   * Various constants used for creating "general legacy" (access 2000-2007)
30   * sort order text index entries.
31   *
32   * @author James Ahlborn
33   */
34  public class GeneralLegacyIndexCodes {
35  
36    static final int MAX_TEXT_INDEX_CHAR_LENGTH =
37      (JetFormat.TEXT_FIELD_MAX_LENGTH / JetFormat.TEXT_FIELD_UNIT_SIZE);
38  
39    static final byte END_TEXT = (byte)0x01;
40    static final byte END_EXTRA_TEXT = (byte)0x00;
41  
42    // unprintable char is removed from normal text.
43    // pattern for unprintable chars in the extra bytes:
44    // 01 01 01 <pos> 06  <code> )
45    // <pos> = 7 + (4 * char_pos) | 0x8000 (as short)
46    // <code> = char code
47    // in the engine, char_pos counts the two byte primary units written so far,
48    // a position in the primary output rather than a count of extra codes
49    // bytes.  the 06 is the type byte of the char's weight table entry and
50    // <code> is that entry's low byte
51    static final int UNPRINTABLE_COUNT_START = 7;
52    static final int UNPRINTABLE_COUNT_MULTIPLIER = 4;
53    static final int UNPRINTABLE_OFFSET_FLAGS = 0x8000;
54    static final byte UNPRINTABLE_MIDFIX = (byte)0x06;
55  
56    // international char is replaced with ascii char.
57    // pattern for international chars in the extra bytes:
58    // [ 02 (for each normal char) ] [ <symbol_code> (for each inat char) ]
59    static final byte INTERNATIONAL_EXTRA_PLACEHOLDER = (byte)0x02;
60  
61    // see Index.writeCrazyCodes for details on writing crazy codes
62    static final byte CRAZY_CODE_START = (byte)0x80;
63    static final byte CRAZY_CODE_1 = (byte)0x02;
64    static final byte CRAZY_CODE_2 = (byte)0x03;
65    // the suffix which follows the crazy codes is
66    // ff 02 [80 x numRepeats] ff [80 x numRepeats], where numRepeats grows with
67    // the number of chars on the crazy path, one repeat per (partial) group of
68    // CRAZY_CODES_PER_SUFFIX_REPEAT chars
69    static final byte CRAZY_CODES_SUFFIX_START = (byte)0xFF;
70    static final byte CRAZY_CODES_SUFFIX_MIDFIX = (byte)0x02;
71    static final byte CRAZY_CODES_SUFFIX_SEPARATOR = (byte)0xFF;
72    static final byte CRAZY_CODES_SUFFIX_REPEAT = (byte)0x80;
73    static final int CRAZY_CODES_PER_SUFFIX_REPEAT = 7;
74    static final byte CRAZY_CODES_UNPRINT_SUFFIX = (byte)0xFF;
75  
76    // stash the codes in some resource files
77    private static final String CODES_FILE =
78      DatabaseImpl.RESOURCE_PATH + "index_codes_genleg.txt";
79    private static final String EXT_CODES_FILE =
80      DatabaseImpl.RESOURCE_PATH + "index_codes_ext_genleg.txt";
81  
82    /**
83     * Enum which classifies the types of char encoding strategies used when
84     * creating text index entries.
85     */
86    enum Type {
87      SIMPLE("S") {
88        @Override public CharHandler parseCodes(String[] codeStrings) {
89          return parseSimpleCodes(codeStrings);
90        }
91      },
92      INTERNATIONAL("I") {
93        @Override public CharHandler parseCodes(String[] codeStrings) {
94          return parseInternationalCodes(codeStrings);
95        }
96      },
97      UNPRINTABLE("U") {
98        @Override public CharHandler parseCodes(String[] codeStrings) {
99          return parseUnprintableCodes(codeStrings);
100       }
101     },
102     UNPRINTABLE_EXT("P") {
103       @Override public CharHandler parseCodes(String[] codeStrings) {
104         return parseUnprintableExtCodes(codeStrings);
105       }
106     },
107     INTERNATIONAL_EXT("Z") {
108       @Override public CharHandler parseCodes(String[] codeStrings) {
109         return parseInternationalExtCodes(codeStrings);
110       }
111     },
112     SIGNIFICANT("G") {
113       @Override public CharHandler parseCodes(String[] codeStrings) {
114         return parseSignificantCodes(codeStrings);
115       }
116     },
117     SURROGATE("Q") {
118       @Override public CharHandler parseCodes(String[] codeStrings) {
119         // these are not parsed from the codes files
120         throw new UnsupportedOperationException();
121       }
122     },
123     IGNORED("X") {
124       @Override public CharHandler parseCodes(String[] codeStrings) {
125         return IGNORED_CHAR_HANDLER;
126       }
127     };
128 
129     private final String _prefixCode;
130 
131     private Type(String prefixCode) {
132       _prefixCode = prefixCode;
133     }
134 
135     public String getPrefixCode() {
136       return _prefixCode;
137     }
138 
139     public abstract CharHandler parseCodes(String[] codeStrings);
140   }
141 
142   /**
143    * Base class for the handlers which hold the text index character encoding
144    * information.
145    */
146   abstract static class CharHandler {
147     public abstract Type getType();
148     public byte[] getInlineBytes(char c) {
149       return null;
150     }
151     public byte[] getExtraBytes() {
152       return null;
153     }
154     public byte[] getUnprintableBytes() {
155       return null;
156     }
157     public byte getExtraByteModifier() {
158       return 0;
159     }
160     public byte getCrazyFlag() {
161       return 0;
162     }
163     public boolean isSignificantChar() {
164       return false;
165     }
166   }
167 
168   /**
169    * CharHandler for Type.SIMPLE
170    */
171   private static final class SimpleCharHandler extends CharHandler {
172     private final byte[] _bytes;
173     private SimpleCharHandler(byte[] bytes) {
174       _bytes = bytes;
175     }
176     @Override public Type getType() {
177       return Type.SIMPLE;
178     }
179     @Override public byte[] getInlineBytes(char c) {
180       return _bytes;
181     }
182   }
183 
184   /**
185    * CharHandler for Type.INTERNATIONAL
186    */
187   private static final class InternationalCharHandler extends CharHandler {
188     private final byte[] _bytes;
189     private final byte[] _extraBytes;
190     private InternationalCharHandler(byte[] bytes, byte[] extraBytes) {
191       _bytes = bytes;
192       _extraBytes = extraBytes;
193     }
194     @Override public Type getType() {
195       return Type.INTERNATIONAL;
196     }
197     @Override public byte[] getInlineBytes(char c) {
198       return _bytes;
199     }
200     @Override public byte[] getExtraBytes() {
201       return _extraBytes;
202     }
203   }
204 
205   /**
206    * CharHandler for Type.UNPRINTABLE
207    */
208   private static final class UnprintableCharHandler extends CharHandler {
209     private final byte[] _unprintBytes;
210     private UnprintableCharHandler(byte[] unprintBytes) {
211       _unprintBytes = unprintBytes;
212     }
213     @Override public Type getType() {
214       return Type.UNPRINTABLE;
215     }
216     @Override public byte[] getUnprintableBytes() {
217       return _unprintBytes;
218     }
219   }
220 
221   /**
222    * CharHandler for Type.UNPRINTABLE_EXT
223    */
224   private static final class UnprintableExtCharHandler extends CharHandler {
225     private final byte _extraByteMod;
226     private UnprintableExtCharHandler(Byte extraByteMod) {
227       _extraByteMod = extraByteMod;
228     }
229     @Override public Type getType() {
230       return Type.UNPRINTABLE_EXT;
231     }
232     @Override public byte getExtraByteModifier() {
233       return _extraByteMod;
234     }
235   }
236 
237   /**
238    * CharHandler for Type.INTERNATIONAL_EXT
239    */
240   private static final class InternationalExtCharHandler extends CharHandler {
241     private final byte[] _bytes;
242     private final byte[] _extraBytes;
243     private final byte _crazyFlag;
244     private InternationalExtCharHandler(byte[] bytes, byte[] extraBytes,
245                                         byte crazyFlag) {
246       _bytes = bytes;
247       _extraBytes = extraBytes;
248       _crazyFlag = crazyFlag;
249     }
250     @Override public Type getType() {
251       return Type.INTERNATIONAL_EXT;
252     }
253     @Override public byte[] getInlineBytes(char c) {
254       return _bytes;
255     }
256     @Override public byte[] getExtraBytes() {
257       return _extraBytes;
258     }
259     @Override public byte getCrazyFlag() {
260       return _crazyFlag;
261     }
262   }
263 
264   /**
265    * CharHandler for Type.SIGNIFICANT
266    */
267   private static final class SignificantCharHandler extends CharHandler {
268     private final byte[] _bytes;
269     private SignificantCharHandler(byte[] bytes) {
270       _bytes = bytes;
271     }
272     @Override public Type getType() {
273       return Type.SIGNIFICANT;
274     }
275     @Override public byte[] getInlineBytes(char c) {
276       return _bytes;
277     }
278     @Override public boolean isSignificantChar() {
279       return true;
280     }
281   }
282 
283   /** shared CharHandler instance for Type.IGNORED */
284   static final CharHandler IGNORED_CHAR_HANDLER = new CharHandler() {
285     @Override public Type getType() {
286       return Type.IGNORED;
287     }
288   };
289 
290   /** the surrogate char bufs are computed on the fly.  re-use a buffer for
291       those */
292   private static final ThreadLocal<byte[]> SURROGATE_CHAR_BUF =
293     ThreadLocal.withInitial(() -> new byte[2]);
294 
295   /**
296    * Supplies the handler for a surrogate char.  The surrogates are not in the
297    * codes files, and which handler a char takes depends on where it falls in
298    * the weight table, so the collations differ here.
299    */
300   interface SurrogateCharHandlers {
301     CharHandler get(char c);
302   }
303 
304   /** the general legacy collation gives a surrogate no weight at all, so both
305       halves of a pair are ignored */
306   static final SurrogateCharHandlers IGNORED_SURROGATES =
307     c -> IGNORED_CHAR_HANDLER;
308 
309   /**
310    * Base for the handlers of the surrogate chars, which are computed rather
311    * than read from the codes files.  Only the general collation weights them,
312    * so the general legacy collation has no subclass of this.
313    */
314   static abstract class SurrogateCharHandler extends CharHandler {
315     private final byte[] _extraBytes;
316 
317     protected SurrogateCharHandler(byte extraByte) {
318       _extraBytes = new byte[]{extraByte};
319     }
320     @Override public Type getType() {
321       return Type.SURROGATE;
322     }
323     @Override public byte[] getExtraBytes() {
324       return _extraBytes;
325     }
326     protected static byte[] toInlineBytes(int idxC) {
327       byte[] bytes = SURROGATE_CHAR_BUF.get();
328       bytes[0] = (byte)((idxC >>> 8) & 0xFF);
329       bytes[1] = (byte)(idxC & 0xFF);
330       return bytes;
331     }
332   }
333 
334   static final char FIRST_CHAR = (char)0x0000;
335   static final char LAST_CHAR = (char)0x00FF;
336   static final char FIRST_EXT_CHAR = LAST_CHAR + 1;
337   static final char LAST_EXT_CHAR = (char)0xFFFF;
338 
339   private static final class Codes
340   {
341     /** handlers for the first 256 chars.  use nested class to lazy load the
342         handlers */
343     private static final CharHandler[] _values = loadCodes(
344         CODES_FILE, FIRST_CHAR, LAST_CHAR);
345   }
346 
347   private static final class ExtCodes
348   {
349     /** handlers for the rest of the chars in BMP 0.  use nested class to
350         lazy load the handlers */
351     private static final CharHandler[] _values = loadCodes(
352         EXT_CODES_FILE, FIRST_EXT_CHAR, LAST_EXT_CHAR);
353   }
354 
355   static final GeneralLegacyIndexCodes GEN_LEG_INSTANCE =
356     new GeneralLegacyIndexCodes();
357 
358   GeneralLegacyIndexCodes() {
359   }
360 
361   /**
362    * Returns the CharHandler for the given character.
363    */
364   CharHandler getCharHandler(char c)
365   {
366     if(c <= LAST_CHAR) {
367       return Codes._values[c];
368     }
369 
370     int extOffset = asUnsignedChar(c) - asUnsignedChar(FIRST_EXT_CHAR);
371     return ExtCodes._values[extOffset];
372   }
373 
374   /**
375    * Loads the CharHandlers for the given range of characters from the
376    * resource file with the given name.
377    */
378   static CharHandler[] loadCodes(String codesFilePath,
379                                  char firstChar, char lastChar)
380   {
381     return loadCodes(codesFilePath, firstChar, lastChar, IGNORED_SURROGATES);
382   }
383 
384   /**
385    * Loads the CharHandlers for the given range of characters from the
386    * resource file with the given name, taking the handlers for the surrogate
387    * chars from the given source.
388    */
389   static CharHandler[] loadCodes(String codesFilePath,
390                                  char firstChar, char lastChar,
391                                  SurrogateCharHandlers surrogates)
392   {
393     int numCodes = (asUnsignedChar(lastChar) - asUnsignedChar(firstChar)) + 1;
394     CharHandler[] values = new CharHandler[numCodes];
395 
396     Map<String,Type> prefixMap = new HashMap<>();
397     for(Type type : Type.values()) {
398       prefixMap.put(type.getPrefixCode(), type);
399     }
400 
401     BufferedReader reader = null;
402     try {
403 
404       reader = new BufferedReader(
405           new InputStreamReader(
406               DatabaseImpl.getResourceAsStream(codesFilePath), "US-ASCII"));
407 
408       int start = asUnsignedChar(firstChar);
409       int end = asUnsignedChar(lastChar);
410       for(int i = start; i <= end; ++i) {
411         char c = (char)i;
412         CharHandler ch = null;
413         if(Character.isHighSurrogate(c) || Character.isLowSurrogate(c)) {
414           // surrogate chars are not included in the codes files
415           ch = surrogates.get(c);
416         } else {
417           ch = parseCodes(prefixMap, reader.readLine());
418         }
419         values[(i - start)] = ch;
420       }
421 
422     } catch(IOException e) {
423       throw new RuntimeException("failed loading index codes file " +
424                                  codesFilePath, e);
425     } finally {
426       ByteUtil.closeQuietly(reader);
427     }
428 
429     return values;
430   }
431 
432   /**
433    * Returns a CharHandler parsed from the given line from an index codes
434    * file.
435    */
436   private static CharHandler parseCodes(Map<String,Type> prefixMap,
437                                         String codeLine)
438   {
439     if(codeLine == null) {
440       throw new IllegalStateException("Invalid codes file");
441     }
442     String prefix = codeLine.substring(0, 1);
443     String suffix = ((codeLine.length() > 1) ? codeLine.substring(1) : "");
444     return prefixMap.get(prefix).parseCodes(suffix.split(",", -1));
445   }
446 
447   /**
448    * Returns a SimpleCharHandler parsed from the given index code strings.
449    */
450   private static CharHandler parseSimpleCodes(String[] codeStrings)
451   {
452     if(codeStrings.length != 1) {
453       throw new IllegalStateException("Unexpected code strings " +
454                                       Arrays.asList(codeStrings));
455     }
456     return new SimpleCharHandler(codesToBytes(codeStrings[0], true));
457   }
458 
459   /**
460    * Returns an InternationalCharHandler parsed from the given index code
461    * strings.
462    */
463   private static CharHandler parseInternationalCodes(String[] codeStrings)
464   {
465     if(codeStrings.length != 2) {
466       throw new IllegalStateException("Unexpected code strings " +
467                                       Arrays.asList(codeStrings));
468     }
469     return new InternationalCharHandler(codesToBytes(codeStrings[0], true),
470                                         codesToBytes(codeStrings[1], true));
471   }
472 
473   /**
474    * Returns a UnprintableCharHandler parsed from the given index code
475    * strings.
476    */
477   private static CharHandler parseUnprintableCodes(String[] codeStrings)
478   {
479     if(codeStrings.length != 1) {
480       throw new IllegalStateException("Unexpected code strings " +
481                                       Arrays.asList(codeStrings));
482     }
483     return new UnprintableCharHandler(codesToBytes(codeStrings[0], true));
484   }
485 
486   /**
487    * Returns a UnprintableExtCharHandler parsed from the given index code
488    * strings.
489    */
490   private static CharHandler parseUnprintableExtCodes(String[] codeStrings)
491   {
492     if(codeStrings.length != 1) {
493       throw new IllegalStateException("Unexpected code strings " +
494                                       Arrays.asList(codeStrings));
495     }
496     byte[] bytes = codesToBytes(codeStrings[0], true);
497     if(bytes.length != 1) {
498       throw new IllegalStateException("Unexpected code strings " +
499                                       Arrays.asList(codeStrings));
500     }
501     return new UnprintableExtCharHandler(bytes[0]);
502   }
503 
504   /**
505    * Returns a InternationalExtCharHandler parsed from the given index code
506    * strings.
507    */
508   private static CharHandler parseInternationalExtCodes(String[] codeStrings)
509   {
510     if(codeStrings.length != 3) {
511       throw new IllegalStateException("Unexpected code strings " +
512                                       Arrays.asList(codeStrings));
513     }
514 
515     byte crazyFlag = ("1".equals(codeStrings[2]) ?
516                       CRAZY_CODE_1 : CRAZY_CODE_2);
517     return new InternationalExtCharHandler(codesToBytes(codeStrings[0], true),
518                                            codesToBytes(codeStrings[1], false),
519                                            crazyFlag);
520   }
521 
522   /**
523    * Returns a SignificantCharHandler parsed from the given index code strings.
524    */
525   private static CharHandler parseSignificantCodes(String[] codeStrings)
526   {
527     if(codeStrings.length != 1) {
528       throw new IllegalStateException("Unexpected code strings " +
529                                       Arrays.asList(codeStrings));
530     }
531     return new SignificantCharHandler(codesToBytes(codeStrings[0], true));
532   }
533 
534   /**
535    * Converts a string of hex encoded bytes to a byte[], optionally throwing
536    * an exception if no codes are given.
537    */
538   private static byte[] codesToBytes(String codes, boolean required)
539   {
540     if(codes.length() == 0) {
541       if(required) {
542         throw new IllegalStateException("empty code bytes");
543       }
544       return null;
545     }
546     if((codes.length() % 2) != 0) {
547       // stripped a leading 0
548       codes = "0" + codes;
549     }
550     byte[] bytes = new byte[codes.length() / 2];
551     for(int i = 0; i < bytes.length; ++i) {
552       int charIdx = i*2;
553       bytes[i] = (byte)(Integer.parseInt(codes.substring(charIdx, charIdx + 2),
554                                          16));
555     }
556     return bytes;
557   }
558 
559   /**
560    * Returns an the char value converted to an unsigned char value.  Note, I
561    * think this is unnecessary (I think java treats chars as unsigned), but I
562    * did this just to be on the safe side.
563    */
564   static int asUnsignedChar(char c)
565   {
566     return c & 0xFFFF;
567   }
568 
569   /**
570    * Converts an index value for a text column into the entry value (which
571    * is based on a variety of nifty codes).
572    */
573   void writeNonNullIndexTextValue(
574       Object value, ByteStream bout, boolean isAscending)
575     throws IOException
576   {
577     // convert to string
578     String str = toIndexCharSequence(value);
579 
580     // record previous entry length so we can do any post-processing
581     // necessary for this entry (handling descending)
582     int prevLength = bout.getLength();
583 
584     // now, convert each character to a "code" of one or more bytes
585     ExtraCodesStream extraCodes = null;
586     ByteStream unprintableCodes = null;
587     ByteStream crazyCodes = null;
588     int charOffset = 0;
589     for(int i = 0; i < str.length(); ++i) {
590 
591       char c = str.charAt(i);
592       CharHandler ch = getCharHandler(c);
593 
594       int curCharOffset = charOffset;
595       byte[] bytes = ch.getInlineBytes(c);
596       if(bytes != null) {
597         // write the "inline" codes immediately
598         bout.write(bytes);
599 
600         // only increment the charOffset for chars with inline codes
601         ++charOffset;
602       }
603 
604       if(ch.getType() == Type.SIMPLE) {
605         // common case, skip further code handling
606         continue;
607       }
608 
609       bytes = ch.getExtraBytes();
610       byte extraCodeModifier = ch.getExtraByteModifier();
611       if((bytes != null) || (extraCodeModifier != 0)) {
612         if(extraCodes == null) {
613           extraCodes = new ExtraCodesStream(str.length());
614         }
615 
616         // keep track of the extra codes for later
617         writeExtraCodes(curCharOffset, bytes, extraCodeModifier, extraCodes);
618       }
619 
620       bytes = ch.getUnprintableBytes();
621       if(bytes != null) {
622         if(unprintableCodes == null) {
623           unprintableCodes = new ByteStream();
624         }
625 
626         // keep track of the unprintable codes for later
627         writeUnprintableCodes(curCharOffset, bytes, unprintableCodes,
628                               extraCodes);
629       }
630 
631       byte crazyFlag = ch.getCrazyFlag();
632       if(crazyFlag != 0) {
633         if(crazyCodes == null) {
634           crazyCodes = new ByteStream();
635         }
636 
637         // keep track of the crazy flags for later
638         crazyCodes.write(crazyFlag);
639       }
640     }
641 
642     // write end text flag
643     bout.write(END_TEXT);
644 
645     boolean hasExtraCodes = trimExtraCodes(
646         extraCodes, (byte)0, INTERNATIONAL_EXTRA_PLACEHOLDER);
647     boolean hasUnprintableCodes = (unprintableCodes != null);
648     boolean hasCrazyCodes = (crazyCodes != null);
649     if(hasExtraCodes || hasUnprintableCodes || hasCrazyCodes) {
650 
651       // we write all the international extra bytes first
652       if(hasExtraCodes) {
653         extraCodes.writeTo(bout);
654       }
655 
656       if(hasCrazyCodes || hasUnprintableCodes) {
657 
658         // write 2 more end flags
659         bout.write(END_TEXT);
660         bout.write(END_TEXT);
661 
662         // next come the crazy flags
663         if(hasCrazyCodes) {
664 
665           writeCrazyCodes(crazyCodes, bout);
666 
667           // if we are writing unprintable codes after this, tack on another
668           // code
669           if(hasUnprintableCodes) {
670             bout.write(CRAZY_CODES_UNPRINT_SUFFIX);
671           }
672         }
673 
674         // then we write all the unprintable extra bytes
675         if(hasUnprintableCodes) {
676 
677           // write another end flag
678           bout.write(END_TEXT);
679 
680           unprintableCodes.writeTo(bout);
681         }
682       }
683     }
684 
685     // handle descending order by inverting the bytes
686     if(!isAscending) {
687 
688       // we actually write the end byte before flipping the bytes, and write
689       // another one after flipping
690       bout.write(END_EXTRA_TEXT);
691 
692       // flip the bytes that we have written thus far for this text value
693       IndexData.flipBytes(bout.getBytes(), prevLength,
694                           (bout.getLength() - prevLength));
695     }
696 
697     // write end extra text
698     bout.write(END_EXTRA_TEXT);
699   }
700 
701   protected static String toIndexCharSequence(Object value)
702       throws IOException {
703 
704     // first, convert to string
705     String str = ColumnImpl.toCharSequence(value).toString();
706 
707     // all text columns (including memos) are only indexed up to the max
708     // number of chars in a VARCHAR column
709     int len = str.length();
710     if(len > MAX_TEXT_INDEX_CHAR_LENGTH) {
711       str = str.substring(0, MAX_TEXT_INDEX_CHAR_LENGTH);
712       len = MAX_TEXT_INDEX_CHAR_LENGTH;
713     }
714 
715     // trailing spaces are ignored for text index entries
716     if((len > 0) && (str.charAt(len - 1) == ' ')) {
717       do {
718         --len;
719       } while((len > 0) && (str.charAt(len - 1) == ' '));
720 
721       str = str.substring(0, len);
722     }
723 
724     return str;
725   }
726 
727   /**
728    * Encodes the given extra code info in the given stream.
729    */
730   private static void writeExtraCodes(
731       int charOffset, byte[] bytes, byte extraCodeModifier,
732       ExtraCodesStream extraCodes)
733   {
734     // we fill in a placeholder value for any chars w/out extra codes
735     int numChars = extraCodes.getNumChars();
736     if(numChars < charOffset) {
737       int fillChars = charOffset - numChars;
738       extraCodes.writeFill(fillChars, INTERNATIONAL_EXTRA_PLACEHOLDER);
739       extraCodes.incrementNumChars(fillChars);
740     }
741 
742     if(bytes != null) {
743 
744       // write the actual extra codes and update the number of chars
745       extraCodes.write(bytes);
746       extraCodes.incrementNumChars(1);
747 
748     } else {
749 
750       // extra code modifiers modify the existing extra code bytes and do not
751       // count as additional extra code chars
752       int lastIdx = extraCodes.getLength() - 1;
753       if(lastIdx >= 0) {
754 
755         // the extra code modifier is added to the last extra code written
756         byte lastByte = extraCodes.get(lastIdx);
757         lastByte += extraCodeModifier;
758         extraCodes.set(lastIdx, lastByte);
759 
760       } else {
761 
762         // there is no previous extra code, add a new code (but keep track of
763         // this "unprintable code" prefix)
764         extraCodes.write(extraCodeModifier);
765         extraCodes.setUnprintablePrefixLen(1);
766       }
767     }
768   }
769 
770   /**
771    * Trims any bytes in the given range off of the end of the given stream,
772    * returning whether or not there are any bytes left in the given stream
773    * after trimming.
774    */
775   private static boolean trimExtraCodes(ByteStream extraCodes,
776                                         byte minTrimCode, byte maxTrimCode)
777   {
778     if(extraCodes == null) {
779       return false;
780     }
781 
782     extraCodes.trimTrailing(minTrimCode, maxTrimCode);
783 
784     // anything left?
785     return (extraCodes.getLength() > 0);
786   }
787 
788   /**
789    * Encodes the given unprintable char codes in the given stream.
790    */
791   private static void writeUnprintableCodes(
792       int charOffset, byte[] bytes, ByteStream unprintableCodes,
793       ExtraCodesStream extraCodes)
794   {
795     // the offset seems to be calculated based on the number of bytes in the
796     // "extra codes" part of the entry (even if there are no extra codes bytes
797     // actually written in the final entry).
798     int unprintCharOffset = charOffset;
799     if(extraCodes != null) {
800       // we need to account for some extra codes which have not been written
801       // yet.  additionally, any unprintable bytes added to the beginning of
802       // the extra codes are ignored.
803       unprintCharOffset = extraCodes.getLength() +
804         (charOffset - extraCodes.getNumChars()) -
805         extraCodes.getUnprintablePrefixLen();
806     }
807 
808     // we write a whacky combo of bytes for each unprintable char which
809     // includes a funky offset and extra char itself
810     int offset =
811       (UNPRINTABLE_COUNT_START +
812        (UNPRINTABLE_COUNT_MULTIPLIER * unprintCharOffset))
813       | UNPRINTABLE_OFFSET_FLAGS;
814 
815     // write offset as big-endian short
816     unprintableCodes.write((offset >> 8) & 0xFF);
817     unprintableCodes.write(offset & 0xFF);
818 
819     unprintableCodes.write(UNPRINTABLE_MIDFIX);
820     unprintableCodes.write(bytes);
821   }
822 
823   /**
824    * Encode the given crazy code bytes into the given byte stream.
825    */
826   private static void writeCrazyCodes(ByteStream crazyCodes, ByteStream bout)
827   {
828     // the suffix length depends on how many chars took the crazy path, which
829     // is the code count before any trimming
830     int numCrazyChars = crazyCodes.getLength();
831 
832     // CRAZY_CODE_2 flags at the end are ignored, so ditch them
833     trimExtraCodes(crazyCodes, CRAZY_CODE_2, CRAZY_CODE_2);
834 
835     if(crazyCodes.getLength() > 0) {
836 
837       // the crazy codes get encoded into 6 bit sequences where each code is 2
838       // bits (where the first 2 bits in the byte are a common prefix).
839       byte curByte = CRAZY_CODE_START;
840       int idx = 0;
841       for(int i = 0; i < crazyCodes.getLength(); ++i) {
842         byte nextByte = crazyCodes.get(i);
843         nextByte <<= ((2 - idx) * 2);
844         curByte |= nextByte;
845 
846         ++idx;
847         if(idx == 3) {
848           // write current byte and reset
849           bout.write(curByte);
850           curByte = CRAZY_CODE_START;
851           idx = 0;
852         }
853       }
854 
855       // write last byte
856       if(idx > 0) {
857         bout.write(curByte);
858       }
859     }
860 
861     // write crazy code suffix (note, we write this even if all the codes are
862     // trimmed
863     writeCrazyCodesSuffix(numCrazyChars, bout);
864   }
865 
866   /**
867    * Encode the suffix which follows the crazy codes into the given byte
868    * stream.
869    */
870   private static void writeCrazyCodesSuffix(int numCrazyChars, ByteStream bout)
871   {
872     int numRepeats =
873       ((numCrazyChars + (CRAZY_CODES_PER_SUFFIX_REPEAT - 1)) /
874        CRAZY_CODES_PER_SUFFIX_REPEAT);
875 
876     bout.write(CRAZY_CODES_SUFFIX_START);
877     bout.write(CRAZY_CODES_SUFFIX_MIDFIX);
878     writeCrazyCodesSuffixRepeats(numRepeats, bout);
879     bout.write(CRAZY_CODES_SUFFIX_SEPARATOR);
880     writeCrazyCodesSuffixRepeats(numRepeats, bout);
881   }
882 
883   private static void writeCrazyCodesSuffixRepeats(int numRepeats,
884                                                    ByteStream bout)
885   {
886     for(int i = 0; i < numRepeats; ++i) {
887       bout.write(CRAZY_CODES_SUFFIX_REPEAT);
888     }
889   }
890 
891   /**
892    * Extension of ByteStream which keeps track of an additional char count and
893    * the length of any "unprintable" code prefix.
894    */
895   private static final class ExtraCodesStream extends ByteStream
896   {
897     private int _numChars;
898     private int _unprintablePrefixLen;
899 
900     private ExtraCodesStream(int length) {
901       super(length);
902     }
903 
904     public int getNumChars() {
905       return _numChars;
906     }
907 
908     public void incrementNumChars(int inc) {
909       _numChars += inc;
910     }
911 
912     public int getUnprintablePrefixLen() {
913       return _unprintablePrefixLen;
914     }
915 
916     public void setUnprintablePrefixLen(int len) {
917       _unprintablePrefixLen = len;
918     }
919   }
920 
921 }