Home | History | Annotate | Download | only in draft
      1 package org.unicode.cldr.draft;
      2 
      3 import java.io.File;
      4 import java.io.Reader;
      5 import java.util.ArrayList;
      6 import java.util.Collection;
      7 import java.util.Collections;
      8 import java.util.EnumMap;
      9 import java.util.HashMap;
     10 import java.util.LinkedHashMap;
     11 import java.util.LinkedHashSet;
     12 import java.util.List;
     13 import java.util.Locale;
     14 import java.util.Map;
     15 import java.util.Map.Entry;
     16 import java.util.Set;
     17 
     18 import org.unicode.cldr.util.CLDRPaths;
     19 import org.unicode.cldr.util.LanguageTagParser;
     20 import org.unicode.cldr.util.LanguageTagParser.Status;
     21 import org.unicode.cldr.util.XMLFileReader;
     22 import org.unicode.cldr.util.XMLFileReader.SimpleHandler;
     23 import org.unicode.cldr.util.XPathParts;
     24 
     25 import com.ibm.icu.dev.util.CollectionUtilities;
     26 import com.ibm.icu.text.UnicodeSet;
     27 
     28 /**
     29  * A first, very rough cut at reading the keyboard data.
     30  * Every public structure is immutable, eg all returned maps, sets.
     31  *
     32  * @author markdavis
     33  */
     34 public class Keyboard {
     35 
     36     private static final boolean DEBUG = false;
     37 
     38     private static final String BASE = CLDRPaths.BASE_DIRECTORY + "keyboards/";
     39 
     40     public enum IsoRow {
     41         E, D, C, B, A;
     42     }
     43 
     44     public enum Iso {
     45         E00, E01, E02, E03, E04, E05, E06, E07, E08, E09, E10, E11, E12, E13, D00, D01, D02, D03, D04, D05, D06, D07, D08, D09, D10, D11, D12, D13, C00, C01, C02, C03, C04, C05, C06, C07, C08, C09, C10, C11, C12, C13, B00, B01, B02, B03, B04, B05, B06, B07, B08, B09, B10, B11, B12, B13, A00, A01, A02, A03, A04, A05, A06, A07, A08, A09, A10, A11, A12, A13;
     46         public final IsoRow isoRow;
     47 
     48         Iso() {
     49             isoRow = IsoRow.valueOf(name().substring(0, 1));
     50         }
     51     }
     52 
     53     // add whatever is needed
     54 
     55     public enum Modifier {
     56         cmd, ctrlL, ctrlR, caps, altL, altR, optL, optR, shiftL, shiftR;
     57     }
     58 
     59     // public static class ModifierSet {
     60     // private String temp; // later on expand into something we can use.
     61     // @Override
     62     // public String toString() {
     63     // return temp;
     64     // }
     65     // @Override
     66     // public boolean equals(Object obj) {
     67     // final ModifierSet other = (ModifierSet)obj;
     68     // return temp.equals(other.temp);
     69     // }
     70     // @Override
     71     // public int hashCode() {
     72     // return temp.hashCode();
     73     // };
     74     //
     75     // /**
     76     // * Parses string like "AltCapsCommand? RShiftCtrl" and returns a set of modifier sets, like:
     77     // * {{RAlt, LAlt, Caps}, {RAlt, LAlt, Caps, Command}, {RShift, LCtrl, RCtrl}}
     78     // */
     79     // public static Set<ModifierSet> parseSet(String input) {
     80     // //ctrl+opt?+caps?+shift? ctrl+cmd?+opt?+shift? ctrl+cmd?+opt?+caps? cmd+ctrl+caps+shift+optL? ...
     81     // Set<ModifierSet> results = new HashSet<ModifierSet>(); // later, Treeset
     82     // if (input != null) {
     83     // for (String ms : input.trim().split(" ")) {
     84     // ModifierSet temp = new ModifierSet();
     85     // temp.temp = ms;
     86     // results.add(temp);
     87     // }
     88     // }
     89     // return results;
     90     // // Set<ModifierSet> current = new LinkedHashSet();EnumSet.noneOf(Modifier.class);
     91     // // for (String mod : input.trim().split("\\+")) {
     92     // // boolean optional = mod.endsWith("?");
     93     // // if (optional) {
     94     // // mod = mod.substring(0,mod.length()-1);
     95     // // }
     96     // // Modifier m = Modifier.valueOf(mod);
     97     // // if (optional) {
     98     // // temp = EnumSet.copyOf(current);
     99     // // } else {
    100     // // for (Modifier m2 : current) {
    101     // // m2.a
    102     // // }
    103     // // }
    104     // // }
    105     // }
    106     // /**
    107     // * Format a set of modifier sets like {{RAlt, LAlt, Caps}, {RAlt, LAlt, Caps, Command}, {RShift, LCtrl, RCtrl}}
    108     // * and return a string like "AltCapsCommand? RShiftCtrl". The exact compaction may vary.
    109     // */
    110     // public static String formatSet(Set<ModifierSet> input) {
    111     // return input.toString();
    112     // }
    113     // }
    114 
    115     public static Set<String> getPlatformIDs() {
    116         Set<String> results = new LinkedHashSet<String>();
    117         File file = new File(BASE);
    118         for (String f : file.list())
    119             if (!f.equals("dtd") && !f.startsWith(".") && !f.startsWith("_")) {
    120                 results.add(f);
    121             }
    122         return results;
    123     }
    124 
    125     public static Set<String> getKeyboardIDs(String platformId) {
    126         Set<String> results = new LinkedHashSet<String>();
    127         File base = new File(BASE + platformId + "/");
    128         for (String f : base.list())
    129             if (f.endsWith(".xml") && !f.startsWith(".") && !f.startsWith("_")) {
    130                 results.add(f.substring(0, f.length() - 4));
    131             }
    132         return results;
    133     }
    134 
    135     public static Platform getPlatform(String platformId) {
    136         final String fileName = BASE + platformId + "/_platform.xml";
    137         try {
    138             final PlatformHandler platformHandler = new PlatformHandler();
    139             new XMLFileReader()
    140                 .setHandler(platformHandler)
    141                 .read(fileName, -1, true);
    142             return platformHandler.getPlatform();
    143         } catch (Exception e) {
    144             throw new KeyboardException(fileName, e);
    145         }
    146     }
    147 
    148     public Keyboard(String locale, String version, String platformVersion, Set<String> names,
    149         Fallback fallback, Set<KeyMap> keyMaps, Map<TransformType, Transforms> transforms) {
    150         this.locale = locale;
    151         this.version = version;
    152         this.platformVersion = platformVersion;
    153         this.fallback = fallback;
    154         this.names = Collections.unmodifiableSet(names);
    155         this.keyMaps = Collections.unmodifiableSet(keyMaps);
    156         this.transforms = Collections.unmodifiableMap(transforms);
    157     }
    158 
    159 //    public static Keyboard getKeyboard(String keyboardId, Set<Exception> errors) {
    160 //        int pos = keyboardId.indexOf("-t-k0-") + 6;
    161 //        int pos2 = keyboardId.indexOf('-', pos);
    162 //        if (pos2 < 0) {
    163 //            pos2 = keyboardId.length();
    164 //        }
    165 //        return getKeyboard(keyboardId.substring(pos, pos2), keyboardId, errors);
    166 //    }
    167 
    168     public static String getPlatformId(String keyboardId) {
    169         int pos = keyboardId.indexOf("-t-k0-") + 6;
    170         int pos2 = keyboardId.indexOf('-', pos);
    171         if (pos2 < 0) {
    172             pos2 = keyboardId.length();
    173         }
    174         return keyboardId.substring(pos, pos2);
    175     }
    176 
    177     public static Keyboard getKeyboard(String platformId, String keyboardId, Set<Exception> errors) {
    178         final String fileName = BASE + platformId + "/" + keyboardId + ".xml";
    179         try {
    180             final KeyboardHandler keyboardHandler = new KeyboardHandler(errors);
    181             new XMLFileReader()
    182                 .setHandler(keyboardHandler)
    183                 .read(fileName, -1, true);
    184             return keyboardHandler.getKeyboard();
    185         } catch (Exception e) {
    186             throw new KeyboardException(fileName + "\n" + CollectionUtilities.join(errors, ", "), e);
    187         }
    188     }
    189 
    190     public static Keyboard getKeyboard(String id, Reader r, Set<Exception> errors) {
    191         //final String fileName = BASE + platformId + "/" + keyboardId + ".xml";
    192         try {
    193             final KeyboardHandler keyboardHandler = new KeyboardHandler(errors);
    194             new XMLFileReader()
    195                 .setHandler(keyboardHandler)
    196                 .read(id, r, -1, true);
    197             return keyboardHandler.getKeyboard();
    198         } catch (Exception e) {
    199             errors.add(e);
    200             return null;
    201         }
    202     }
    203 
    204     public static class Platform {
    205         final String id;
    206         final Map<String, Iso> hardwareMap;
    207 
    208         public String getId() {
    209             return id;
    210         }
    211 
    212         public Map<String, Iso> getHardwareMap() {
    213             return hardwareMap;
    214         }
    215 
    216         public Platform(String id, Map<String, Iso> hardwareMap) {
    217             super();
    218             this.id = id;
    219             this.hardwareMap = Collections.unmodifiableMap(hardwareMap);
    220         }
    221     }
    222 
    223     public enum Gesture {
    224         LONGPRESS;
    225         public static Gesture fromString(String string) {
    226             return Gesture.valueOf(string.toUpperCase(Locale.ENGLISH));
    227         }
    228     }
    229 
    230     public enum TransformStatus {
    231         DEFAULT, NO;
    232         public static TransformStatus fromString(String string) {
    233             return string == null ? TransformStatus.DEFAULT : TransformStatus.valueOf(string
    234                 .toUpperCase(Locale.ENGLISH));
    235         }
    236     }
    237 
    238     public enum TransformType {
    239         SIMPLE;
    240         public static TransformType forString(String string) {
    241             return string == null ? TransformType.SIMPLE : TransformType.valueOf(string.toUpperCase(Locale.ENGLISH));
    242         }
    243     }
    244 
    245     public static class Output {
    246         final String output;
    247         final TransformStatus transformStatus;
    248         final Map<Gesture, List<String>> gestures;
    249 
    250         public Output(String output, Map<Gesture, List<String>> gestures, TransformStatus transformStatus) {
    251             this.output = output;
    252             this.transformStatus = transformStatus;
    253             this.gestures = Collections.unmodifiableMap(gestures); // TODO make lists unmodifiable
    254         }
    255 
    256         public String getOutput() {
    257             return output;
    258         }
    259 
    260         public TransformStatus getTransformStatus() {
    261             return transformStatus;
    262         }
    263 
    264         public Map<Gesture, List<String>> getGestures() {
    265             return gestures;
    266         }
    267 
    268         public String toString() {
    269             return "{" + output + "," + transformStatus + ", " + gestures + "}";
    270         }
    271     }
    272 
    273     public static class KeyMap {
    274         private final KeyboardModifierSet modifiers;
    275         final Map<Iso, Output> iso2output;
    276 
    277         public KeyMap(KeyboardModifierSet keyMapModifiers, Map<Iso, Output> data) {
    278             this.modifiers = keyMapModifiers;
    279             this.iso2output = Collections.unmodifiableMap(data);
    280         }
    281 
    282         public KeyboardModifierSet getModifiers() {
    283             return modifiers;
    284         }
    285 
    286         public Map<Iso, Output> getIso2Output() {
    287             return iso2output;
    288         }
    289 
    290         public String toString() {
    291             return "{" + modifiers + "," + iso2output + "}";
    292         }
    293     }
    294 
    295     public static class Transforms {
    296         final Map<String, String> string2string;
    297 
    298         public Transforms(Map<String, String> data) {
    299             this.string2string = data;
    300         }
    301 
    302         public Map<String, String> getMatch(String prefix) {
    303             Map<String, String> results = new LinkedHashMap<String, String>();
    304             for (Entry<String, String> entry : string2string.entrySet()) {
    305                 String key = entry.getKey();
    306                 if (key.startsWith(prefix)) {
    307                     results.put(key.substring(prefix.length()), entry.getValue());
    308                 }
    309             }
    310             return results;
    311         }
    312     }
    313 
    314     private final String locale;
    315     private final String version;
    316     private final String platformVersion;
    317     private final Fallback fallback;
    318     private final Set<String> names;
    319     private final Set<KeyMap> keyMaps;
    320     private final Map<TransformType, Transforms> transforms;
    321 
    322     public String getLocaleId() {
    323         return locale;
    324     }
    325 
    326     public String getVersion() {
    327         return version;
    328     }
    329 
    330     public String getPlatformVersion() {
    331         return platformVersion;
    332     }
    333 
    334     public Fallback getFallback() {
    335         return fallback;
    336     }
    337 
    338     public Set<String> getNames() {
    339         return names;
    340     }
    341 
    342     public Set<KeyMap> getKeyMaps() {
    343         return keyMaps;
    344     }
    345 
    346     public Map<TransformType, Transforms> getTransforms() {
    347         return transforms;
    348     }
    349 
    350     /**
    351      * Return all possible results. Could be external utility. WARNING: doesn't account for transform='no' or
    352      * failure='omit'.
    353      */
    354     public UnicodeSet getPossibleResults() {
    355         UnicodeSet results = new UnicodeSet();
    356         for (KeyMap keymap : getKeyMaps()) {
    357             addOutput(keymap.iso2output.values(), results);
    358         }
    359         for (Transforms transforms : getTransforms().values()) {
    360             // loop, to catch empty case
    361             for (String result : transforms.string2string.values()) {
    362                 if (!result.isEmpty()) {
    363                     results.add(result);
    364                 }
    365             }
    366         }
    367         return results;
    368     }
    369 
    370     private void addOutput(Collection<Output> values, UnicodeSet results) {
    371         for (Output value : values) {
    372             if (value.output != null && !value.output.isEmpty()) {
    373                 results.add(value.output);
    374             }
    375             for (List<String> outputList : value.gestures.values()) {
    376                 results.addAll(outputList);
    377             }
    378         }
    379     }
    380 
    381     private static class PlatformHandler extends SimpleHandler {
    382         String id;
    383         Map<String, Iso> hardwareMap = new HashMap<String, Iso>();
    384         XPathParts parts = new XPathParts();
    385 
    386         public void handlePathValue(String path, String value) {
    387             parts.set(path);
    388             // <platform id='android'/>
    389             id = parts.getAttributeValue(0, "id");
    390             if (parts.size() > 1) {
    391                 String element1 = parts.getElement(1);
    392                 // <platform> <hardwareMap> <map keycode='0' iso='C01'/>
    393                 if (element1.equals("hardwareMap")) {
    394                     hardwareMap.put(parts.getAttributeValue(2, "keycode"),
    395                         Iso.valueOf(parts.getAttributeValue(2, "iso")));
    396                 }
    397             }
    398         };
    399 
    400         public Platform getPlatform() {
    401             return new Platform(id, hardwareMap);
    402         }
    403     }
    404 
    405     public enum Fallback {
    406         BASE, OMIT;
    407         public static Fallback forString(String string) {
    408             return string == null ? Fallback.BASE : Fallback.valueOf(string.toUpperCase(Locale.ENGLISH));
    409         }
    410     }
    411 
    412     private static class KeyboardHandler extends SimpleHandler {
    413         Set<Exception> errors; //  = new LinkedHashSet<Exception>();
    414         Set<String> errors2 = new LinkedHashSet<String>();
    415         // doesn't do any error checking for collisions, etc. yet.
    416         String locale; // TODO
    417         String version; // TODO
    418         String platformVersion; // TODO
    419 
    420         Set<String> names = new LinkedHashSet<String>();
    421         Fallback fallback = Fallback.BASE;
    422 
    423         KeyboardModifierSet keyMapModifiers = null;
    424         Map<Iso, Output> iso2output = new EnumMap<Iso, Output>(Iso.class);
    425         Set<KeyMap> keyMaps = new LinkedHashSet<KeyMap>();
    426 
    427         TransformType currentType = null;
    428         Map<String, String> currentTransforms = null;
    429         Map<TransformType, Transforms> transformMap = new EnumMap<TransformType, Transforms>(TransformType.class);
    430 
    431         XPathParts parts = new XPathParts();
    432         LanguageTagParser ltp = new LanguageTagParser();
    433 
    434         public KeyboardHandler(Set<Exception> errorsOutput) {
    435             errors = errorsOutput;
    436             errors.clear();
    437         }
    438 
    439         public Keyboard getKeyboard() {
    440             // finish everything off
    441             addToKeyMaps();
    442             if (currentType != null) {
    443                 transformMap.put(currentType, new Transforms(currentTransforms));
    444             }
    445 //            errors.clear();
    446 //            errors.addAll(this.errors);
    447             return new Keyboard(locale, version, platformVersion, names, fallback, keyMaps, transformMap);
    448         }
    449 
    450         public void handlePathValue(String path, String value) {
    451             // System.out.println(path);
    452             try {
    453                 parts.set(path);
    454                 if (locale == null) {
    455                     // <keyboard locale='bg-t-k0-chromeos-phonetic'>
    456                     locale = parts.getAttributeValue(0, "locale");
    457                     ltp.set(locale);
    458                     Map<String, String> extensions = ltp.getExtensions();
    459                     LanguageTagParser.Status status = ltp.getStatus(errors2);
    460                     if (errors2.size() != 0 || !extensions.containsKey("t")) {
    461                         errors.add(new KeyboardException("Bad locale tag: " + locale + ", " + errors2.toString()));
    462                     } else if (status != Status.MINIMAL) {
    463                         errors.add(new KeyboardWarningException("Non-minimal locale tag: " + locale));
    464                     }
    465                 }
    466                 String element1 = parts.getElement(1);
    467                 if (element1.equals("baseMap")) {
    468                     // <baseMap fallback='true'>/ <map iso="E00" chars=""/> <!-- ` -->
    469                     Iso iso = Iso.valueOf(parts.getAttributeValue(2, "iso"));
    470                     if (DEBUG) {
    471                         System.out.println("baseMap: iso=" + iso + ";");
    472                     }
    473                     final Output output = getOutput();
    474                     if (output != null) {
    475                         iso2output.put(iso, output);
    476                     }
    477                 } else if (element1.equals("keyMap")) {
    478                     // <keyMap modifiers='shift+caps?'><map base="" chars="!"/> <!-- 1 -->
    479                     final String modifiers = parts.getAttributeValue(1, "modifiers");
    480                     KeyboardModifierSet newMods = KeyboardModifierSet.parseSet(modifiers == null ? "" : modifiers);
    481                     if (!newMods.equals(keyMapModifiers)) {
    482                         if (keyMapModifiers != null) {
    483                             addToKeyMaps();
    484                         }
    485                         iso2output = new LinkedHashMap<Iso, Output>();
    486                         keyMapModifiers = newMods;
    487                     }
    488                     String isoString = parts.getAttributeValue(2, "iso");
    489                     if (DEBUG) {
    490                         System.out.println("keyMap: base=" + isoString + ";");
    491                     }
    492                     final Output output = getOutput();
    493                     if (output != null) {
    494                         iso2output.put(Iso.valueOf(isoString), output);
    495                     }
    496                 } else if (element1.equals("transforms")) {
    497                     // <transforms type='simple'> <transform from="` " to="`"/>
    498                     TransformType type = TransformType.forString(parts.getAttributeValue(1, "type"));
    499                     if (type != currentType) {
    500                         if (currentType != null) {
    501                             transformMap.put(currentType, new Transforms(currentTransforms));
    502                         }
    503                         currentType = type;
    504                         currentTransforms = new LinkedHashMap<String, String>();
    505                     }
    506                     final String from = fixValue(parts.getAttributeValue(2, "from"));
    507                     final String to = fixValue(parts.getAttributeValue(2, "to"));
    508                     if (from.equals(to)) {
    509                         errors.add(new KeyboardException("Illegal transform from:" + from + " to:" + to));
    510                     }
    511                     if (DEBUG) {
    512                         System.out.println("transform: from=" + from + ";\tto=" + to + ";");
    513                     }
    514                     // if (result.isEmpty()) {
    515                     // System.out.println("**Empty result at " + path);
    516                     // }
    517                     currentTransforms.put(from, to);
    518                 } else if (element1.equals("version")) {
    519                     // <version platform='0.17' number='$Revision$'/>
    520                     platformVersion = parts.getAttributeValue(1, "platform");
    521                     version = parts.getAttributeValue(1, "number");
    522                 } else if (element1.equals("names")) {
    523                     // <names> <name value='cs'/>
    524                     names.add(parts.getAttributeValue(2, "value"));
    525                 } else if (element1.equals("settings")) {
    526                     // <settings fallback='omit'/>
    527                     fallback = Fallback.forString(parts.getAttributeValue(1, "fallback"));
    528                 } else {
    529                     throw new KeyboardException("Unexpected element: " + element1);
    530                 }
    531             } catch (Exception e) {
    532                 throw new KeyboardException("Unexpected error in: " + path, e);
    533             }
    534         }
    535 
    536         public void addToKeyMaps() {
    537             for (KeyMap item : keyMaps) {
    538                 if (item.modifiers.containsSome(keyMapModifiers)) {
    539                     errors.add(new KeyboardException("Modifier overlap: " + item.modifiers + " already contains " + keyMapModifiers));
    540                 }
    541                 if (item.iso2output.equals(iso2output)) {
    542                     errors.add(new KeyboardException("duplicate keyboard: " + item.modifiers + " has same layout as " + keyMapModifiers));
    543                 }
    544             }
    545             keyMaps.add(new KeyMap(keyMapModifiers, iso2output));
    546         }
    547 
    548         private String fixValue(String value) {
    549             StringBuilder b = new StringBuilder();
    550             int last = 0;
    551             while (true) {
    552                 int pos = value.indexOf("\\u{", last);
    553                 if (pos < 0) {
    554                     break;
    555                 }
    556                 int posEnd = value.indexOf("}", pos + 3);
    557                 if (posEnd < 0) {
    558                     break;
    559                 }
    560                 b.append(value.substring(last, pos)).appendCodePoint(
    561                     Integer.parseInt(value.substring(pos + 3, posEnd), 16));
    562                 last = posEnd + 1;
    563             }
    564             b.append(value.substring(last));
    565             return b.toString();
    566         }
    567 
    568         public Output getOutput() {
    569             String chars = null;
    570             TransformStatus transformStatus = TransformStatus.DEFAULT;
    571             Map<Gesture, List<String>> gestures = new EnumMap<Gesture, List<String>>(Gesture.class);
    572 
    573             for (Entry<String, String> attributeAndValue : parts.getAttributes(-1).entrySet()) {
    574                 String attribute = attributeAndValue.getKey();
    575                 String attributeValue = attributeAndValue.getValue();
    576                 if (attribute.equals("to")) {
    577                     chars = fixValue(attributeValue);
    578                     if (DEBUG) {
    579                         System.out.println("\tchars=" + chars + ";");
    580                     }
    581                     if (chars.isEmpty()) {
    582                         errors.add(new KeyboardException("**Empty result at " + parts.toString()));
    583                     }
    584                 } else if (attribute.equals("transform")) {
    585                     transformStatus = TransformStatus.fromString(attributeValue);
    586                 } else if (attribute.equals("iso") || attribute.equals("base")) {
    587                     // ignore, handled above
    588                 } else {
    589                     LinkedHashSet<String> list = new LinkedHashSet<String>();
    590                     for (String item : attributeValue.trim().split(" ")) {
    591                         final String fixedValue = fixValue(item);
    592                         if (fixedValue.isEmpty()) {
    593                             // throw new KeyboardException("Null string in list. " + parts);
    594                             continue;
    595                         }
    596                         list.add(fixedValue);
    597                     }
    598                     gestures.put(Gesture.fromString(attribute),
    599                         Collections.unmodifiableList(new ArrayList<String>(list)));
    600                     if (DEBUG) {
    601                         System.out.println("\tgesture=" + attribute + ";\tto=" + list + ";");
    602                     }
    603                 }
    604             }
    605             return new Output(chars, gestures, transformStatus);
    606         };
    607     }
    608 
    609     public static class KeyboardException extends RuntimeException {
    610         private static final long serialVersionUID = 3802627982169201480L;
    611 
    612         public KeyboardException(String string) {
    613             super(string);
    614         }
    615 
    616         public KeyboardException(String string, Exception e) {
    617             super(string, e);
    618         }
    619     }
    620 
    621     public static class KeyboardWarningException extends KeyboardException {
    622         private static final long serialVersionUID = 3802627982169201480L;
    623 
    624         public KeyboardWarningException(String string) {
    625             super(string);
    626         }
    627 
    628         public KeyboardWarningException(String string, Exception e) {
    629             super(string, e);
    630         }
    631     }
    632 
    633 }
    634