• 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.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