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.google.common.base.Joiner; 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 Iso()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 getPlatformIDs()115 public static Set<String> getPlatformIDs() { 116 Set<String> results = new LinkedHashSet<>(); 117 File file = new File(BASE); 118 for (String f : file.list()) { 119 if (!f.equals("dtd") && !f.startsWith(".") && !f.startsWith("_") && !f.equals("README.md")) { 120 results.add(f); 121 } 122 } 123 return results; 124 } 125 getKeyboardIDs(String platformId)126 public static Set<String> getKeyboardIDs(String platformId) { 127 Set<String> results = new LinkedHashSet<>(); 128 File base = new File(BASE + platformId + "/"); 129 for (String f : base.list()) { 130 if (f.endsWith(".xml") && !f.startsWith(".") && !f.startsWith("_") && !f.equals("README.md")) { 131 results.add(f.substring(0, f.length() - 4)); 132 } 133 } 134 return results; 135 } 136 getPlatform(String platformId)137 public static Platform getPlatform(String platformId) { 138 final String fileName = BASE + platformId + "/_platform.xml"; 139 try { 140 final PlatformHandler platformHandler = new PlatformHandler(); 141 new XMLFileReader() 142 .setHandler(platformHandler) 143 .read(fileName, -1, true); 144 return platformHandler.getPlatform(); 145 } catch (Exception e) { 146 throw new KeyboardException(fileName, e); 147 } 148 } 149 Keyboard(String locale, String version, String platformVersion, Set<String> names, Fallback fallback, Set<KeyMap> keyMaps, Map<TransformType, Transforms> transforms)150 public Keyboard(String locale, String version, String platformVersion, Set<String> names, 151 Fallback fallback, Set<KeyMap> keyMaps, Map<TransformType, Transforms> transforms) { 152 this.locale = locale; 153 this.version = version; 154 this.platformVersion = platformVersion; 155 this.fallback = fallback; 156 this.names = Collections.unmodifiableSet(names); 157 this.keyMaps = Collections.unmodifiableSet(keyMaps); 158 this.transforms = Collections.unmodifiableMap(transforms); 159 } 160 161 // public static Keyboard getKeyboard(String keyboardId, Set<Exception> errors) { 162 // int pos = keyboardId.indexOf("-t-k0-") + 6; 163 // int pos2 = keyboardId.indexOf('-', pos); 164 // if (pos2 < 0) { 165 // pos2 = keyboardId.length(); 166 // } 167 // return getKeyboard(keyboardId.substring(pos, pos2), keyboardId, errors); 168 // } 169 getPlatformId(String keyboardId)170 public static String getPlatformId(String keyboardId) { 171 int pos = keyboardId.indexOf("-t-k0-") + 6; 172 int pos2 = keyboardId.indexOf('-', pos); 173 if (pos2 < 0) { 174 pos2 = keyboardId.length(); 175 } 176 return keyboardId.substring(pos, pos2); 177 } 178 getKeyboard(String platformId, String keyboardId, Set<Exception> errors)179 public static Keyboard getKeyboard(String platformId, String keyboardId, Set<Exception> errors) { 180 final String fileName = BASE + platformId + "/" + keyboardId + ".xml"; 181 try { 182 final KeyboardHandler keyboardHandler = new KeyboardHandler(errors); 183 new XMLFileReader() 184 .setHandler(keyboardHandler) 185 .read(fileName, -1, true); 186 return keyboardHandler.getKeyboard(); 187 } catch (Exception e) { 188 throw new KeyboardException(fileName + "\n" + Joiner.on(", ").join(errors), e); 189 } 190 } 191 getKeyboard(String id, Reader r, Set<Exception> errors)192 public static Keyboard getKeyboard(String id, Reader r, Set<Exception> errors) { 193 //final String fileName = BASE + platformId + "/" + keyboardId + ".xml"; 194 try { 195 final KeyboardHandler keyboardHandler = new KeyboardHandler(errors); 196 new XMLFileReader() 197 .setHandler(keyboardHandler) 198 .read(id, r, -1, true); 199 return keyboardHandler.getKeyboard(); 200 } catch (Exception e) { 201 errors.add(e); 202 return null; 203 } 204 } 205 206 public static class Platform { 207 final String id; 208 final Map<String, Iso> hardwareMap; 209 getId()210 public String getId() { 211 return id; 212 } 213 getHardwareMap()214 public Map<String, Iso> getHardwareMap() { 215 return hardwareMap; 216 } 217 Platform(String id, Map<String, Iso> hardwareMap)218 public Platform(String id, Map<String, Iso> hardwareMap) { 219 super(); 220 this.id = id; 221 this.hardwareMap = Collections.unmodifiableMap(hardwareMap); 222 } 223 } 224 225 public enum Gesture { 226 LONGPRESS; fromString(String string)227 public static Gesture fromString(String string) { 228 return Gesture.valueOf(string.toUpperCase(Locale.ENGLISH)); 229 } 230 } 231 232 public enum TransformStatus { 233 DEFAULT, NO; fromString(String string)234 public static TransformStatus fromString(String string) { 235 return string == null ? TransformStatus.DEFAULT : TransformStatus.valueOf(string 236 .toUpperCase(Locale.ENGLISH)); 237 } 238 } 239 240 public enum TransformType { 241 SIMPLE; forString(String string)242 public static TransformType forString(String string) { 243 return string == null ? TransformType.SIMPLE : TransformType.valueOf(string.toUpperCase(Locale.ENGLISH)); 244 } 245 } 246 247 public static class Output { 248 final String output; 249 final TransformStatus transformStatus; 250 final Map<Gesture, List<String>> gestures; 251 Output(String output, Map<Gesture, List<String>> gestures, TransformStatus transformStatus)252 public Output(String output, Map<Gesture, List<String>> gestures, TransformStatus transformStatus) { 253 this.output = output; 254 this.transformStatus = transformStatus; 255 this.gestures = Collections.unmodifiableMap(gestures); // TODO make lists unmodifiable 256 } 257 getOutput()258 public String getOutput() { 259 return output; 260 } 261 getTransformStatus()262 public TransformStatus getTransformStatus() { 263 return transformStatus; 264 } 265 getGestures()266 public Map<Gesture, List<String>> getGestures() { 267 return gestures; 268 } 269 270 @Override toString()271 public String toString() { 272 return "{" + output + "," + transformStatus + ", " + gestures + "}"; 273 } 274 } 275 276 public static class KeyMap { 277 private final KeyboardModifierSet modifiers; 278 final Map<Iso, Output> iso2output; 279 KeyMap(KeyboardModifierSet keyMapModifiers, Map<Iso, Output> data)280 public KeyMap(KeyboardModifierSet keyMapModifiers, Map<Iso, Output> data) { 281 this.modifiers = keyMapModifiers; 282 this.iso2output = Collections.unmodifiableMap(data); 283 } 284 getModifiers()285 public KeyboardModifierSet getModifiers() { 286 return modifiers; 287 } 288 getIso2Output()289 public Map<Iso, Output> getIso2Output() { 290 return iso2output; 291 } 292 293 @Override toString()294 public String toString() { 295 return "{" + modifiers + "," + iso2output + "}"; 296 } 297 } 298 299 public static class Transforms { 300 final Map<String, String> string2string; 301 Transforms(Map<String, String> data)302 public Transforms(Map<String, String> data) { 303 this.string2string = data; 304 } 305 getMatch(String prefix)306 public Map<String, String> getMatch(String prefix) { 307 Map<String, String> results = new LinkedHashMap<>(); 308 for (Entry<String, String> entry : string2string.entrySet()) { 309 String key = entry.getKey(); 310 if (key.startsWith(prefix)) { 311 results.put(key.substring(prefix.length()), entry.getValue()); 312 } 313 } 314 return results; 315 } 316 } 317 318 private final String locale; 319 private final String version; 320 private final String platformVersion; 321 private final Fallback fallback; 322 private final Set<String> names; 323 private final Set<KeyMap> keyMaps; 324 private final Map<TransformType, Transforms> transforms; 325 getLocaleId()326 public String getLocaleId() { 327 return locale; 328 } 329 getVersion()330 public String getVersion() { 331 return version; 332 } 333 getPlatformVersion()334 public String getPlatformVersion() { 335 return platformVersion; 336 } 337 getFallback()338 public Fallback getFallback() { 339 return fallback; 340 } 341 getNames()342 public Set<String> getNames() { 343 return names; 344 } 345 getKeyMaps()346 public Set<KeyMap> getKeyMaps() { 347 return keyMaps; 348 } 349 getTransforms()350 public Map<TransformType, Transforms> getTransforms() { 351 return transforms; 352 } 353 354 /** 355 * Return all possible results. Could be external utility. WARNING: doesn't account for transform='no' or 356 * failure='omit'. 357 */ getPossibleResults()358 public UnicodeSet getPossibleResults() { 359 UnicodeSet results = new UnicodeSet(); 360 for (KeyMap keymap : getKeyMaps()) { 361 addOutput(keymap.iso2output.values(), results); 362 } 363 for (Transforms transforms : getTransforms().values()) { 364 // loop, to catch empty case 365 for (String result : transforms.string2string.values()) { 366 if (!result.isEmpty()) { 367 results.add(result); 368 } 369 } 370 } 371 return results; 372 } 373 addOutput(Collection<Output> values, UnicodeSet results)374 private void addOutput(Collection<Output> values, UnicodeSet results) { 375 for (Output value : values) { 376 if (value.output != null && !value.output.isEmpty()) { 377 results.add(value.output); 378 } 379 for (List<String> outputList : value.gestures.values()) { 380 results.addAll(outputList); 381 } 382 } 383 } 384 385 private static class PlatformHandler extends SimpleHandler { 386 String id; 387 Map<String, Iso> hardwareMap = new HashMap<>(); 388 389 @Override handlePathValue(String path, @SuppressWarnings("unused") String value)390 public void handlePathValue(String path, @SuppressWarnings("unused") String value) { 391 XPathParts parts = XPathParts.getFrozenInstance(path); 392 // <platform id='android'/> 393 id = parts.getAttributeValue(0, "id"); 394 if (parts.size() > 1) { 395 String element1 = parts.getElement(1); 396 // <platform> <hardwareMap> <map keycode='0' iso='C01'/> 397 if (element1.equals("hardwareMap")) { 398 hardwareMap.put(parts.getAttributeValue(2, "keycode"), 399 Iso.valueOf(parts.getAttributeValue(2, "iso"))); 400 } 401 } 402 } 403 getPlatform()404 public Platform getPlatform() { 405 return new Platform(id, hardwareMap); 406 } 407 } 408 409 public enum Fallback { 410 BASE, OMIT; forString(String string)411 public static Fallback forString(String string) { 412 return string == null ? Fallback.BASE : Fallback.valueOf(string.toUpperCase(Locale.ENGLISH)); 413 } 414 } 415 416 private static class KeyboardHandler extends SimpleHandler { 417 Set<Exception> errors; // = new LinkedHashSet<Exception>(); 418 Set<String> errors2 = new LinkedHashSet<>(); 419 // doesn't do any error checking for collisions, etc. yet. 420 String locale; // TODO 421 String version; // TODO 422 String platformVersion; // TODO 423 424 Set<String> names = new LinkedHashSet<>(); 425 Fallback fallback = Fallback.BASE; 426 427 KeyboardModifierSet keyMapModifiers = null; 428 Map<Iso, Output> iso2output = new EnumMap<>(Iso.class); 429 Set<KeyMap> keyMaps = new LinkedHashSet<>(); 430 431 TransformType currentType = null; 432 Map<String, String> currentTransforms = null; 433 Map<TransformType, Transforms> transformMap = new EnumMap<>(TransformType.class); 434 435 LanguageTagParser ltp = new LanguageTagParser(); 436 KeyboardHandler(Set<Exception> errorsOutput)437 public KeyboardHandler(Set<Exception> errorsOutput) { 438 errors = errorsOutput; 439 errors.clear(); 440 } 441 getKeyboard()442 public Keyboard getKeyboard() { 443 // finish everything off 444 addToKeyMaps(); 445 if (currentType != null) { 446 transformMap.put(currentType, new Transforms(currentTransforms)); 447 } 448 return new Keyboard(locale, version, platformVersion, names, fallback, keyMaps, transformMap); 449 } 450 451 @Override handlePathValue(String path, @SuppressWarnings("unused") String value)452 public void handlePathValue(String path, @SuppressWarnings("unused") String value) { 453 try { 454 XPathParts parts = XPathParts.getFrozenInstance(path); 455 if (locale == null) { 456 // <keyboard locale='bg-t-k0-chromeos-phonetic'> 457 locale = parts.getAttributeValue(0, "locale"); 458 ltp.set(locale); 459 Map<String, String> extensions = ltp.getExtensions(); 460 LanguageTagParser.Status status = ltp.getStatus(errors2); 461 if (errors2.size() != 0 || !ltp.hasT()) { 462 errors.add(new KeyboardException("Bad locale tag: " + locale + ", " + errors2.toString())); 463 } else if (status != Status.MINIMAL) { 464 errors.add(new KeyboardWarningException("Non-minimal locale tag: " + locale)); 465 } 466 } 467 String element1 = parts.getElement(1); 468 if (element1.equals("baseMap")) { 469 // <baseMap fallback='true'>/ <map iso="E00" chars="ـ"/> <!-- ` --> 470 Iso iso = Iso.valueOf(parts.getAttributeValue(2, "iso")); 471 if (DEBUG) { 472 System.out.println("baseMap: iso=" + iso + ";"); 473 } 474 final Output output = getOutput(parts); 475 if (output != null) { 476 iso2output.put(iso, output); 477 } 478 } else if (element1.equals("keyMap")) { 479 // <keyMap modifiers='shift+caps?'><map base="١" chars="!"/> <!-- 1 --> 480 final String modifiers = parts.getAttributeValue(1, "modifiers"); 481 KeyboardModifierSet newMods = KeyboardModifierSet.parseSet(modifiers == null ? "" : modifiers); 482 if (!newMods.equals(keyMapModifiers)) { 483 if (keyMapModifiers != null) { 484 addToKeyMaps(); 485 } 486 iso2output = new LinkedHashMap<>(); 487 keyMapModifiers = newMods; 488 } 489 String isoString = parts.getAttributeValue(2, "iso"); 490 if (DEBUG) { 491 System.out.println("keyMap: base=" + isoString + ";"); 492 } 493 final Output output = getOutput(parts); 494 if (output != null) { 495 iso2output.put(Iso.valueOf(isoString), output); 496 } 497 } else if (element1.equals("transforms")) { 498 // <transforms type='simple'> <transform from="` " to="`"/> 499 TransformType type = TransformType.forString(parts.getAttributeValue(1, "type")); 500 if (type != currentType) { 501 if (currentType != null) { 502 transformMap.put(currentType, new Transforms(currentTransforms)); 503 } 504 currentType = type; 505 currentTransforms = new LinkedHashMap<>(); 506 } 507 final String from = fixValue(parts.getAttributeValue(2, "from")); 508 final String to = fixValue(parts.getAttributeValue(2, "to")); 509 if (from.equals(to)) { 510 errors.add(new KeyboardException("Illegal transform from:" + from + " to:" + to)); 511 } 512 if (DEBUG) { 513 System.out.println("transform: from=" + from + ";\tto=" + to + ";"); 514 } 515 // if (result.isEmpty()) { 516 // System.out.println("**Empty result at " + path); 517 // } 518 currentTransforms.put(from, to); 519 } else if (element1.equals("version")) { 520 // <version platform='0.17' number='$Revision$'/> 521 platformVersion = parts.getAttributeValue(1, "platform"); 522 version = parts.getAttributeValue(1, "number"); 523 } else if (element1.equals("names")) { 524 // <names> <name value='cs'/> 525 names.add(parts.getAttributeValue(2, "value")); 526 } else if (element1.equals("settings")) { 527 // <settings fallback='omit'/> 528 fallback = Fallback.forString(parts.getAttributeValue(1, "fallback")); 529 } else { 530 throw new KeyboardException("Unexpected element: " + element1); 531 } 532 } catch (Exception e) { 533 throw new KeyboardException("Unexpected error in: " + path, e); 534 } 535 } 536 addToKeyMaps()537 public void addToKeyMaps() { 538 for (KeyMap item : keyMaps) { 539 if (item.modifiers.containsSome(keyMapModifiers)) { 540 errors.add(new KeyboardException("Modifier overlap: " + item.modifiers + " already contains " + keyMapModifiers)); 541 } 542 if (item.iso2output.equals(iso2output)) { 543 errors.add(new KeyboardException("duplicate keyboard: " + item.modifiers + " has same layout as " + keyMapModifiers)); 544 } 545 } 546 keyMaps.add(new KeyMap(keyMapModifiers, iso2output)); 547 } 548 fixValue(String value)549 private String fixValue(String value) { 550 StringBuilder b = new StringBuilder(); 551 int last = 0; 552 while (true) { 553 int pos = value.indexOf("\\u{", last); 554 if (pos < 0) { 555 break; 556 } 557 int posEnd = value.indexOf("}", pos + 3); 558 if (posEnd < 0) { 559 break; 560 } 561 b.append(value.substring(last, pos)).appendCodePoint( 562 Integer.parseInt(value.substring(pos + 3, posEnd), 16)); 563 last = posEnd + 1; 564 } 565 b.append(value.substring(last)); 566 return b.toString(); 567 } 568 getOutput(XPathParts parts)569 public Output getOutput(XPathParts parts) { 570 String chars = null; 571 TransformStatus transformStatus = TransformStatus.DEFAULT; 572 Map<Gesture, List<String>> gestures = new EnumMap<>(Gesture.class); 573 574 for (Entry<String, String> attributeAndValue : parts.getAttributes(-1).entrySet()) { 575 String attribute = attributeAndValue.getKey(); 576 String attributeValue = attributeAndValue.getValue(); 577 if (attribute.equals("to")) { 578 chars = fixValue(attributeValue); 579 if (DEBUG) { 580 System.out.println("\tchars=" + chars + ";"); 581 } 582 if (chars.isEmpty()) { 583 errors.add(new KeyboardException("**Empty result at " + parts.toString())); 584 } 585 } else if (attribute.equals("transform")) { 586 transformStatus = TransformStatus.fromString(attributeValue); 587 } else if (attribute.equals("iso") || attribute.equals("base")) { 588 // ignore, handled above 589 } else { 590 LinkedHashSet<String> list = new LinkedHashSet<>(); 591 for (String item : attributeValue.trim().split(" ")) { 592 final String fixedValue = fixValue(item); 593 if (fixedValue.isEmpty()) { 594 // throw new KeyboardException("Null string in list. " + parts); 595 continue; 596 } 597 list.add(fixedValue); 598 } 599 gestures.put(Gesture.fromString(attribute), 600 Collections.unmodifiableList(new ArrayList<>(list))); 601 if (DEBUG) { 602 System.out.println("\tgesture=" + attribute + ";\tto=" + list + ";"); 603 } 604 } 605 } 606 return new Output(chars, gestures, transformStatus); 607 } 608 } 609 610 public static class KeyboardException extends RuntimeException { 611 private static final long serialVersionUID = 3802627982169201480L; 612 KeyboardException(String string)613 public KeyboardException(String string) { 614 super(string); 615 } 616 KeyboardException(String string, Exception e)617 public KeyboardException(String string, Exception e) { 618 super(string, e); 619 } 620 } 621 622 public static class KeyboardWarningException extends KeyboardException { 623 private static final long serialVersionUID = 3802627982169201480L; 624 KeyboardWarningException(String string)625 public KeyboardWarningException(String string) { 626 super(string); 627 } 628 KeyboardWarningException(String string, Exception e)629 public KeyboardWarningException(String string, Exception e) { 630 super(string, e); 631 } 632 } 633 634 } 635