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 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<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 getKeyboardIDs(String platformId)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 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" + CollectionUtilities.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 toString()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 KeyMap(KeyboardModifierSet keyMapModifiers, Map<Iso, Output> data)277 public KeyMap(KeyboardModifierSet keyMapModifiers, Map<Iso, Output> data) { 278 this.modifiers = keyMapModifiers; 279 this.iso2output = Collections.unmodifiableMap(data); 280 } 281 getModifiers()282 public KeyboardModifierSet getModifiers() { 283 return modifiers; 284 } 285 getIso2Output()286 public Map<Iso, Output> getIso2Output() { 287 return iso2output; 288 } 289 toString()290 public String toString() { 291 return "{" + modifiers + "," + iso2output + "}"; 292 } 293 } 294 295 public static class Transforms { 296 final Map<String, String> string2string; 297 Transforms(Map<String, String> data)298 public Transforms(Map<String, String> data) { 299 this.string2string = data; 300 } 301 getMatch(String prefix)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 getLocaleId()322 public String getLocaleId() { 323 return locale; 324 } 325 getVersion()326 public String getVersion() { 327 return version; 328 } 329 getPlatformVersion()330 public String getPlatformVersion() { 331 return platformVersion; 332 } 333 getFallback()334 public Fallback getFallback() { 335 return fallback; 336 } 337 getNames()338 public Set<String> getNames() { 339 return names; 340 } 341 getKeyMaps()342 public Set<KeyMap> getKeyMaps() { 343 return keyMaps; 344 } 345 getTransforms()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 */ getPossibleResults()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 addOutput(Collection<Output> values, UnicodeSet results)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 handlePathValue(String path, String value)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 getPlatform()400 public Platform getPlatform() { 401 return new Platform(id, hardwareMap); 402 } 403 } 404 405 public enum Fallback { 406 BASE, OMIT; forString(String string)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 KeyboardHandler(Set<Exception> errorsOutput)434 public KeyboardHandler(Set<Exception> errorsOutput) { 435 errors = errorsOutput; 436 errors.clear(); 437 } 438 getKeyboard()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 handlePathValue(String path, String value)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 addToKeyMaps()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 fixValue(String value)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 getOutput()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 KeyboardException(String string)612 public KeyboardException(String string) { 613 super(string); 614 } 615 KeyboardException(String string, Exception e)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 KeyboardWarningException(String string)624 public KeyboardWarningException(String string) { 625 super(string); 626 } 627 KeyboardWarningException(String string, Exception e)628 public KeyboardWarningException(String string, Exception e) { 629 super(string, e); 630 } 631 } 632 633 } 634