package org.unicode.cldr.draft; import java.io.File; import java.io.Reader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.unicode.cldr.util.CLDRPaths; import org.unicode.cldr.util.LanguageTagParser; import org.unicode.cldr.util.LanguageTagParser.Status; import org.unicode.cldr.util.XMLFileReader; import org.unicode.cldr.util.XMLFileReader.SimpleHandler; import org.unicode.cldr.util.XPathParts; import com.ibm.icu.dev.util.CollectionUtilities; import com.ibm.icu.text.UnicodeSet; /** * A first, very rough cut at reading the keyboard data. * Every public structure is immutable, eg all returned maps, sets. * * @author markdavis */ public class Keyboard { private static final boolean DEBUG = false; private static final String BASE = CLDRPaths.BASE_DIRECTORY + "keyboards/"; public enum IsoRow { E, D, C, B, A; } public enum Iso { 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; public final IsoRow isoRow; Iso() { isoRow = IsoRow.valueOf(name().substring(0, 1)); } } // add whatever is needed public enum Modifier { cmd, ctrlL, ctrlR, caps, altL, altR, optL, optR, shiftL, shiftR; } // public static class ModifierSet { // private String temp; // later on expand into something we can use. // @Override // public String toString() { // return temp; // } // @Override // public boolean equals(Object obj) { // final ModifierSet other = (ModifierSet)obj; // return temp.equals(other.temp); // } // @Override // public int hashCode() { // return temp.hashCode(); // }; // // /** // * Parses string like "AltCapsCommand? RShiftCtrl" and returns a set of modifier sets, like: // * {{RAlt, LAlt, Caps}, {RAlt, LAlt, Caps, Command}, {RShift, LCtrl, RCtrl}} // */ // public static Set parseSet(String input) { // //ctrl+opt?+caps?+shift? ctrl+cmd?+opt?+shift? ctrl+cmd?+opt?+caps? cmd+ctrl+caps+shift+optL? ... // Set results = new HashSet(); // later, Treeset // if (input != null) { // for (String ms : input.trim().split(" ")) { // ModifierSet temp = new ModifierSet(); // temp.temp = ms; // results.add(temp); // } // } // return results; // // Set current = new LinkedHashSet();EnumSet.noneOf(Modifier.class); // // for (String mod : input.trim().split("\\+")) { // // boolean optional = mod.endsWith("?"); // // if (optional) { // // mod = mod.substring(0,mod.length()-1); // // } // // Modifier m = Modifier.valueOf(mod); // // if (optional) { // // temp = EnumSet.copyOf(current); // // } else { // // for (Modifier m2 : current) { // // m2.a // // } // // } // // } // } // /** // * Format a set of modifier sets like {{RAlt, LAlt, Caps}, {RAlt, LAlt, Caps, Command}, {RShift, LCtrl, RCtrl}} // * and return a string like "AltCapsCommand? RShiftCtrl". The exact compaction may vary. // */ // public static String formatSet(Set input) { // return input.toString(); // } // } public static Set getPlatformIDs() { Set results = new LinkedHashSet(); File file = new File(BASE); for (String f : file.list()) if (!f.equals("dtd") && !f.startsWith(".") && !f.startsWith("_")) { results.add(f); } return results; } public static Set getKeyboardIDs(String platformId) { Set results = new LinkedHashSet(); File base = new File(BASE + platformId + "/"); for (String f : base.list()) if (f.endsWith(".xml") && !f.startsWith(".") && !f.startsWith("_")) { results.add(f.substring(0, f.length() - 4)); } return results; } public static Platform getPlatform(String platformId) { final String fileName = BASE + platformId + "/_platform.xml"; try { final PlatformHandler platformHandler = new PlatformHandler(); new XMLFileReader() .setHandler(platformHandler) .read(fileName, -1, true); return platformHandler.getPlatform(); } catch (Exception e) { throw new KeyboardException(fileName, e); } } public Keyboard(String locale, String version, String platformVersion, Set names, Fallback fallback, Set keyMaps, Map transforms) { this.locale = locale; this.version = version; this.platformVersion = platformVersion; this.fallback = fallback; this.names = Collections.unmodifiableSet(names); this.keyMaps = Collections.unmodifiableSet(keyMaps); this.transforms = Collections.unmodifiableMap(transforms); } // public static Keyboard getKeyboard(String keyboardId, Set errors) { // int pos = keyboardId.indexOf("-t-k0-") + 6; // int pos2 = keyboardId.indexOf('-', pos); // if (pos2 < 0) { // pos2 = keyboardId.length(); // } // return getKeyboard(keyboardId.substring(pos, pos2), keyboardId, errors); // } public static String getPlatformId(String keyboardId) { int pos = keyboardId.indexOf("-t-k0-") + 6; int pos2 = keyboardId.indexOf('-', pos); if (pos2 < 0) { pos2 = keyboardId.length(); } return keyboardId.substring(pos, pos2); } public static Keyboard getKeyboard(String platformId, String keyboardId, Set errors) { final String fileName = BASE + platformId + "/" + keyboardId + ".xml"; try { final KeyboardHandler keyboardHandler = new KeyboardHandler(errors); new XMLFileReader() .setHandler(keyboardHandler) .read(fileName, -1, true); return keyboardHandler.getKeyboard(); } catch (Exception e) { throw new KeyboardException(fileName + "\n" + CollectionUtilities.join(errors, ", "), e); } } public static Keyboard getKeyboard(String id, Reader r, Set errors) { //final String fileName = BASE + platformId + "/" + keyboardId + ".xml"; try { final KeyboardHandler keyboardHandler = new KeyboardHandler(errors); new XMLFileReader() .setHandler(keyboardHandler) .read(id, r, -1, true); return keyboardHandler.getKeyboard(); } catch (Exception e) { errors.add(e); return null; } } public static class Platform { final String id; final Map hardwareMap; public String getId() { return id; } public Map getHardwareMap() { return hardwareMap; } public Platform(String id, Map hardwareMap) { super(); this.id = id; this.hardwareMap = Collections.unmodifiableMap(hardwareMap); } } public enum Gesture { LONGPRESS; public static Gesture fromString(String string) { return Gesture.valueOf(string.toUpperCase(Locale.ENGLISH)); } } public enum TransformStatus { DEFAULT, NO; public static TransformStatus fromString(String string) { return string == null ? TransformStatus.DEFAULT : TransformStatus.valueOf(string .toUpperCase(Locale.ENGLISH)); } } public enum TransformType { SIMPLE; public static TransformType forString(String string) { return string == null ? TransformType.SIMPLE : TransformType.valueOf(string.toUpperCase(Locale.ENGLISH)); } } public static class Output { final String output; final TransformStatus transformStatus; final Map> gestures; public Output(String output, Map> gestures, TransformStatus transformStatus) { this.output = output; this.transformStatus = transformStatus; this.gestures = Collections.unmodifiableMap(gestures); // TODO make lists unmodifiable } public String getOutput() { return output; } public TransformStatus getTransformStatus() { return transformStatus; } public Map> getGestures() { return gestures; } public String toString() { return "{" + output + "," + transformStatus + ", " + gestures + "}"; } } public static class KeyMap { private final KeyboardModifierSet modifiers; final Map iso2output; public KeyMap(KeyboardModifierSet keyMapModifiers, Map data) { this.modifiers = keyMapModifiers; this.iso2output = Collections.unmodifiableMap(data); } public KeyboardModifierSet getModifiers() { return modifiers; } public Map getIso2Output() { return iso2output; } public String toString() { return "{" + modifiers + "," + iso2output + "}"; } } public static class Transforms { final Map string2string; public Transforms(Map data) { this.string2string = data; } public Map getMatch(String prefix) { Map results = new LinkedHashMap(); for (Entry entry : string2string.entrySet()) { String key = entry.getKey(); if (key.startsWith(prefix)) { results.put(key.substring(prefix.length()), entry.getValue()); } } return results; } } private final String locale; private final String version; private final String platformVersion; private final Fallback fallback; private final Set names; private final Set keyMaps; private final Map transforms; public String getLocaleId() { return locale; } public String getVersion() { return version; } public String getPlatformVersion() { return platformVersion; } public Fallback getFallback() { return fallback; } public Set getNames() { return names; } public Set getKeyMaps() { return keyMaps; } public Map getTransforms() { return transforms; } /** * Return all possible results. Could be external utility. WARNING: doesn't account for transform='no' or * failure='omit'. */ public UnicodeSet getPossibleResults() { UnicodeSet results = new UnicodeSet(); for (KeyMap keymap : getKeyMaps()) { addOutput(keymap.iso2output.values(), results); } for (Transforms transforms : getTransforms().values()) { // loop, to catch empty case for (String result : transforms.string2string.values()) { if (!result.isEmpty()) { results.add(result); } } } return results; } private void addOutput(Collection values, UnicodeSet results) { for (Output value : values) { if (value.output != null && !value.output.isEmpty()) { results.add(value.output); } for (List outputList : value.gestures.values()) { results.addAll(outputList); } } } private static class PlatformHandler extends SimpleHandler { String id; Map hardwareMap = new HashMap(); public void handlePathValue(String path, @SuppressWarnings("unused") String value) { XPathParts parts = XPathParts.getFrozenInstance(path); // id = parts.getAttributeValue(0, "id"); if (parts.size() > 1) { String element1 = parts.getElement(1); // if (element1.equals("hardwareMap")) { hardwareMap.put(parts.getAttributeValue(2, "keycode"), Iso.valueOf(parts.getAttributeValue(2, "iso"))); } } }; public Platform getPlatform() { return new Platform(id, hardwareMap); } } public enum Fallback { BASE, OMIT; public static Fallback forString(String string) { return string == null ? Fallback.BASE : Fallback.valueOf(string.toUpperCase(Locale.ENGLISH)); } } private static class KeyboardHandler extends SimpleHandler { Set errors; // = new LinkedHashSet(); Set errors2 = new LinkedHashSet(); // doesn't do any error checking for collisions, etc. yet. String locale; // TODO String version; // TODO String platformVersion; // TODO Set names = new LinkedHashSet(); Fallback fallback = Fallback.BASE; KeyboardModifierSet keyMapModifiers = null; Map iso2output = new EnumMap(Iso.class); Set keyMaps = new LinkedHashSet(); TransformType currentType = null; Map currentTransforms = null; Map transformMap = new EnumMap(TransformType.class); LanguageTagParser ltp = new LanguageTagParser(); public KeyboardHandler(Set errorsOutput) { errors = errorsOutput; errors.clear(); } public Keyboard getKeyboard() { // finish everything off addToKeyMaps(); if (currentType != null) { transformMap.put(currentType, new Transforms(currentTransforms)); } return new Keyboard(locale, version, platformVersion, names, fallback, keyMaps, transformMap); } public void handlePathValue(String path, @SuppressWarnings("unused") String value) { try { XPathParts parts = XPathParts.getFrozenInstance(path); if (locale == null) { // locale = parts.getAttributeValue(0, "locale"); ltp.set(locale); Map extensions = ltp.getExtensions(); LanguageTagParser.Status status = ltp.getStatus(errors2); if (errors2.size() != 0 || !ltp.hasT()) { errors.add(new KeyboardException("Bad locale tag: " + locale + ", " + errors2.toString())); } else if (status != Status.MINIMAL) { errors.add(new KeyboardWarningException("Non-minimal locale tag: " + locale)); } } String element1 = parts.getElement(1); if (element1.equals("baseMap")) { // / Iso iso = Iso.valueOf(parts.getAttributeValue(2, "iso")); if (DEBUG) { System.out.println("baseMap: iso=" + iso + ";"); } final Output output = getOutput(parts); if (output != null) { iso2output.put(iso, output); } } else if (element1.equals("keyMap")) { // final String modifiers = parts.getAttributeValue(1, "modifiers"); KeyboardModifierSet newMods = KeyboardModifierSet.parseSet(modifiers == null ? "" : modifiers); if (!newMods.equals(keyMapModifiers)) { if (keyMapModifiers != null) { addToKeyMaps(); } iso2output = new LinkedHashMap(); keyMapModifiers = newMods; } String isoString = parts.getAttributeValue(2, "iso"); if (DEBUG) { System.out.println("keyMap: base=" + isoString + ";"); } final Output output = getOutput(parts); if (output != null) { iso2output.put(Iso.valueOf(isoString), output); } } else if (element1.equals("transforms")) { // TransformType type = TransformType.forString(parts.getAttributeValue(1, "type")); if (type != currentType) { if (currentType != null) { transformMap.put(currentType, new Transforms(currentTransforms)); } currentType = type; currentTransforms = new LinkedHashMap(); } final String from = fixValue(parts.getAttributeValue(2, "from")); final String to = fixValue(parts.getAttributeValue(2, "to")); if (from.equals(to)) { errors.add(new KeyboardException("Illegal transform from:" + from + " to:" + to)); } if (DEBUG) { System.out.println("transform: from=" + from + ";\tto=" + to + ";"); } // if (result.isEmpty()) { // System.out.println("**Empty result at " + path); // } currentTransforms.put(from, to); } else if (element1.equals("version")) { // platformVersion = parts.getAttributeValue(1, "platform"); version = parts.getAttributeValue(1, "number"); } else if (element1.equals("names")) { // names.add(parts.getAttributeValue(2, "value")); } else if (element1.equals("settings")) { // fallback = Fallback.forString(parts.getAttributeValue(1, "fallback")); } else { throw new KeyboardException("Unexpected element: " + element1); } } catch (Exception e) { throw new KeyboardException("Unexpected error in: " + path, e); } } public void addToKeyMaps() { for (KeyMap item : keyMaps) { if (item.modifiers.containsSome(keyMapModifiers)) { errors.add(new KeyboardException("Modifier overlap: " + item.modifiers + " already contains " + keyMapModifiers)); } if (item.iso2output.equals(iso2output)) { errors.add(new KeyboardException("duplicate keyboard: " + item.modifiers + " has same layout as " + keyMapModifiers)); } } keyMaps.add(new KeyMap(keyMapModifiers, iso2output)); } private String fixValue(String value) { StringBuilder b = new StringBuilder(); int last = 0; while (true) { int pos = value.indexOf("\\u{", last); if (pos < 0) { break; } int posEnd = value.indexOf("}", pos + 3); if (posEnd < 0) { break; } b.append(value.substring(last, pos)).appendCodePoint( Integer.parseInt(value.substring(pos + 3, posEnd), 16)); last = posEnd + 1; } b.append(value.substring(last)); return b.toString(); } public Output getOutput(XPathParts parts) { String chars = null; TransformStatus transformStatus = TransformStatus.DEFAULT; Map> gestures = new EnumMap>(Gesture.class); for (Entry attributeAndValue : parts.getAttributes(-1).entrySet()) { String attribute = attributeAndValue.getKey(); String attributeValue = attributeAndValue.getValue(); if (attribute.equals("to")) { chars = fixValue(attributeValue); if (DEBUG) { System.out.println("\tchars=" + chars + ";"); } if (chars.isEmpty()) { errors.add(new KeyboardException("**Empty result at " + parts.toString())); } } else if (attribute.equals("transform")) { transformStatus = TransformStatus.fromString(attributeValue); } else if (attribute.equals("iso") || attribute.equals("base")) { // ignore, handled above } else { LinkedHashSet list = new LinkedHashSet(); for (String item : attributeValue.trim().split(" ")) { final String fixedValue = fixValue(item); if (fixedValue.isEmpty()) { // throw new KeyboardException("Null string in list. " + parts); continue; } list.add(fixedValue); } gestures.put(Gesture.fromString(attribute), Collections.unmodifiableList(new ArrayList(list))); if (DEBUG) { System.out.println("\tgesture=" + attribute + ";\tto=" + list + ";"); } } } return new Output(chars, gestures, transformStatus); }; } public static class KeyboardException extends RuntimeException { private static final long serialVersionUID = 3802627982169201480L; public KeyboardException(String string) { super(string); } public KeyboardException(String string, Exception e) { super(string, e); } } public static class KeyboardWarningException extends KeyboardException { private static final long serialVersionUID = 3802627982169201480L; public KeyboardWarningException(String string) { super(string); } public KeyboardWarningException(String string, Exception e) { super(string, e); } } }