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