• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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