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