IndexCodes.java

/*
Copyright (c) 2008 Health Market Science, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.healthmarketscience.jackcess.impl;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The entry flags every index writes, and the map from the sort order of a
 * text column to the collation which encodes it.  A collation is a weight
 * table of one of the three families, with the moves of a tailoring over it
 * where the locale has one.
 *
 * @author James Ahlborn
 */
public class IndexCodes {

  /** the french locales, whose collation needs the reversal in
      {@link FrenchIndexCodes} and no data */
  private static final Set<Short> FRENCH_LCIDS = Set.of(
      (short)0x040C,   // france
      (short)0x080C,   // belgium
      (short)0x0C0C,   // canada
      (short)0x100C,   // switzerland
      (short)0x140C,   // luxembourg
      (short)0x180C);  // monaco

  /** the LCIDs which ms access collates exactly as 1033, so a weight table
      serves them with no tailoring at all.  Serbian latin 0x081A is left out:
      jet 4 leaves it untailored and ACE gives it the slovenian collation, so
      it stays unsupported in both.
      <p>
      Ms access resolves a locale to a collation when the sort order is chosen,
      so it writes 0x0409 for every locale below and a file is not expected to
      carry one of these.  The table is here so that a file which does carry
      one is read with the collation it names rather than refused. */
  private static final Set<Short> UNTAILORED_LCIDS = Set.of(
    (short)0x0401, // arabic (saudi arabia)
    (short)0x0402, // bulgarian
    (short)0x0403, // catalan
    (short)0x0407, // german (germany)
    (short)0x0408, // greek
    (short)0x0409, // english (united states)
    (short)0x040D, // hebrew
    (short)0x0410, // italian (italy)
    (short)0x0413, // dutch (netherlands)
    (short)0x0416, // portuguese (brazil)
    (short)0x0419, // russian
    (short)0x041C, // albanian
    (short)0x0420, // urdu (pakistan)
    (short)0x0421, // indonesian
    (short)0x0423, // belarusian
    (short)0x0429, // persian
    (short)0x042D, // basque
    (short)0x0436, // afrikaans
    (short)0x0437, // georgian
    (short)0x0438, // faroese
    (short)0x0439, // hindi
    (short)0x043E, // malay (malaysia)
    (short)0x0441, // swahili
    (short)0x0801, // arabic (iraq)
    (short)0x0807, // german (switzerland)
    (short)0x0809, // english (united kingdom)
    (short)0x0810, // italian (switzerland)
    (short)0x0813, // dutch (belgium)
    (short)0x0816, // portuguese (portugal)
    (short)0x083E, // malay (brunei)
    (short)0x0C01, // arabic (egypt)
    (short)0x0C07, // german (austria)
    (short)0x0C09, // english (australia)
    (short)0x0C1A, // serbian cyrillic (serbia)
    (short)0x1001, // arabic (libya)
    (short)0x1007, // german (luxembourg)
    (short)0x1009, // english (canada)
    (short)0x1401, // arabic (algeria)
    (short)0x1407, // german (liechtenstein)
    (short)0x1409, // english (new zealand)
    (short)0x1801, // arabic (morocco)
    (short)0x1809, // english (ireland)
    (short)0x1C01, // arabic (tunisia)
    (short)0x1C09, // english (south africa)
    (short)0x2001, // arabic (oman)
    (short)0x2009, // english (jamaica)
    (short)0x2401, // arabic (yemen)
    (short)0x2409, // english (caribbean)
    (short)0x2801, // arabic (syria)
    (short)0x2809, // english (belize)
    (short)0x2C01, // arabic (jordan)
    (short)0x2C09, // english (trinidad and tobago)
    (short)0x3001, // arabic (lebanon)
    (short)0x3009, // english (zimbabwe)
    (short)0x3401, // arabic (kuwait)
    (short)0x3409, // english (philippines)
    (short)0x3801, // arabic (united arab emirates)
    (short)0x3C01); // arabic (bahrain)

  /** the collations which move a few characters against the general legacy
      weight table, by every LCID which ms access resolves to each one.  The
      moves are in a resource file named after the collation, which
      {@link TailoredIndexCodes} loads.
      <p>
      A collation belongs here only if moving characters is the whole of it.
      One which also collapses a sequence of characters to one weight needs
      more than this table, and its moves alone would write a wrong entry for
      every value holding such a sequence. */
  private static final Map<Short,String> TAILORED_LCIDS = Map.ofEntries(
      Map.entry((short)0x0405, "czech"),
      Map.entry((short)0x0406, "norwegian"),   // danish
      Map.entry((short)0x040A, "spanish_traditional"),
      Map.entry((short)0x040B, "swedish"),     // finnish
      Map.entry((short)0x040E, "hungarian"),
      Map.entry((short)0x040F, "icelandic"),
      Map.entry((short)0x0414, "norwegian"),
      Map.entry((short)0x0415, "polish"),
      Map.entry((short)0x0418, "romanian"),
      Map.entry((short)0x041A, "croatian"),
      Map.entry((short)0x041B, "slovak"),
      Map.entry((short)0x041D, "swedish"),
      Map.entry((short)0x041F, "turkish"),
      Map.entry((short)0x0422, "ukrainian"),
      Map.entry((short)0x0424, "slovenian"),
      Map.entry((short)0x0425, "estonian"),
      Map.entry((short)0x0426, "latvian"),
      Map.entry((short)0x0427, "lithuanian"),
      Map.entry((short)0x042A, "vietnamese"),
      Map.entry((short)0x042F, "macedonian"),
      Map.entry((short)0x0814, "norwegian"),   // norwegian nynorsk
      Map.entry((short)0x081D, "swedish"),     // swedish (finland)
      Map.entry((short)0x080A, "spanish_traditional"), // spanish (mexico)
      Map.entry((short)0x0827, "lithuanian"),
      // spanish modern, and the latin american sublanguages which sort as it
      Map.entry((short)0x0C0A, "spanish_modern"),  // spain, modern sort
      Map.entry((short)0x100A, "spanish_modern"),  // guatemala
      Map.entry((short)0x140A, "spanish_modern"),  // costa rica
      Map.entry((short)0x180A, "spanish_modern"),  // panama
      Map.entry((short)0x1C0A, "spanish_modern"),  // dominican republic
      Map.entry((short)0x200A, "spanish_modern"),  // venezuela
      Map.entry((short)0x240A, "spanish_modern"),  // colombia
      Map.entry((short)0x280A, "spanish_modern"),  // peru
      Map.entry((short)0x2C0A, "spanish_modern"),  // argentina
      Map.entry((short)0x300A, "spanish_modern"),  // ecuador
      Map.entry((short)0x340A, "spanish_modern"),  // chile
      Map.entry((short)0x380A, "spanish_modern"),  // uruguay
      Map.entry((short)0x3C0A, "spanish_modern"));  // paraguay

  /** the collations which a whole sort id names rather than an LCID: a
      variant of a locale, such as german phone book against plain german, and
      a locale in the general family.  The high byte of the sort id is the
      family, so it says which weight table the moves are against. */
  private static final Map<Integer,String> TAILORED_SORT_IDS = Map.of(
      0x00010407, "german_phonebook",
      0x0001040E, "hungarian_technical",
      0x00010437, "georgian_modern",
      0x01000418, "romanian_general",
      0x0100041A, "croatian_general",
      0x01000439, "indic",
      0x0100081A, "serbian",
      0x0100141A, "bosnian");

  /** the tailorings which have been loaded, by the family and name which
      also name the resource file, since a database uses one collation and
      loading all of them would read files nothing needs */
  private static final Map<String,GeneralLegacyIndexCodes> TAILORINGS =
    new ConcurrentHashMap<>();

  static final byte ASC_START_FLAG = (byte)0x7F;
  static final byte ASC_NULL_FLAG = (byte)0x00;
  static final byte DESC_START_FLAG = (byte)0x80;
  static final byte DESC_NULL_FLAG = (byte)0xFF;

  static final byte ASC_BOOLEAN_TRUE = (byte)0x00;
  static final byte ASC_BOOLEAN_FALSE = (byte)0xFF;
  
  static final byte DESC_BOOLEAN_TRUE = ASC_BOOLEAN_FALSE;
  static final byte DESC_BOOLEAN_FALSE = ASC_BOOLEAN_TRUE;


  static boolean isNullEntry(byte startEntryFlag) {
    return((startEntryFlag == ASC_NULL_FLAG) ||
           (startEntryFlag == DESC_NULL_FLAG));
  }
  
  static byte getNullEntryFlag(boolean isAscending) {
    return(isAscending ? ASC_NULL_FLAG : DESC_NULL_FLAG);
  }
  
  static byte getStartEntryFlag(boolean isAscending) {
    return(isAscending ? ASC_START_FLAG : DESC_START_FLAG);
  }

  /**
   * Returns the index codes which encode text in the given collation, or
   * {@code null} if jackcess cannot write an index in it.
   */
  static GeneralLegacyIndexCodes getTextIndexCodes(
      ColumnImpl.SortOrder sortOrder)
  {
    short version = sortOrder.getVersion();

    // the general 97 order has no sort id, and no collation of it is known
    // beyond the untailored one below
    if(version >= 0) {
      String collation = TAILORED_SORT_IDS.get(sortOrder.getSortId());
      if(collation != null) {
        return loadTailoring(collation, version);
      }
    }

    // a variant the table above does not name has no measured data.  it
    // orders differently from the plain locale, so the tailoring of the
    // locale would write wrong entries for it
    if(sortOrder.getVariant() != 0) {
      return null;
    }

    // french changes no weight, so it needs the reversal and no data.  it
    // exists only in the general legacy family, which is what the version
    // test says
    if(FRENCH_LCIDS.contains(sortOrder.getValue()) &&
       (version == ColumnImpl.GENERAL_LEGACY_SORT_ORDER.getVersion())) {
      return FrenchIndexCodes.FRENCH_INSTANCE;
    }

    if(version == ColumnImpl.GENERAL_LEGACY_SORT_ORDER.getVersion()) {
      String locale = TAILORED_LCIDS.get(sortOrder.getValue());
      if(locale != null) {
        return loadTailoring(locale, version);
      }
    }

    // the general 97 order is jet 3, which the jet 4 locale map behind the
    // untailored list does not describe.  A russian jet 3 file weighs even
    // the latin letters differently from an english one, so only the english
    // order is known
    if(version == ColumnImpl.GENERAL_97_SORT_ORDER.getVersion()) {
      return ((sortOrder.getValue() ==
               ColumnImpl.GENERAL_97_SORT_ORDER.getValue()) ?
              General97IndexCodes.GEN_97_INSTANCE : null);
    }

    if(!UNTAILORED_LCIDS.contains(sortOrder.getValue())) {
      return null;
    }

    // the locale needs no tailoring, so the weight table of its family is the
    // whole of its collation
    if(version == ColumnImpl.GENERAL_SORT_ORDER.getVersion()) {
      return GeneralIndexCodes.GEN_INSTANCE;
    }
    if(version == ColumnImpl.GENERAL_LEGACY_SORT_ORDER.getVersion()) {
      return GeneralLegacyIndexCodes.GEN_LEG_INSTANCE;
    }
    return null;
  }

  /**
   * Returns the index codes of the collation with the given name, against the
   * weight table of the given family.
   */
  private static GeneralLegacyIndexCodes loadTailoring(String collation,
                                                       short version)
  {
    boolean isGeneral =
      (version == ColumnImpl.GENERAL_SORT_ORDER.getVersion());
    String family = (isGeneral ? GeneralIndexCodes.GEN_FAMILY :
                     GeneralLegacyIndexCodes.GEN_LEG_FAMILY);
    return TAILORINGS.computeIfAbsent(
        family + "_" + collation,
        key -> (isGeneral ?
                TailoredIndexCodes.load(collation,
                                        GeneralIndexCodes.GEN_INSTANCE,
                                        family) :
                TailoredIndexCodes.load(
                    collation, GeneralLegacyIndexCodes.GEN_LEG_INSTANCE,
                    family)));
  }
}