blob: 9f40d4d6dc8741e36179679812eb17d828ac5769 [file] [log] [blame]
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<ModifierSet> parseSet(String input) {
// //ctrl+opt?+caps?+shift? ctrl+cmd?+opt?+shift? ctrl+cmd?+opt?+caps? cmd+ctrl+caps+shift+optL? ...
// Set<ModifierSet> results = new HashSet<ModifierSet>(); // later, Treeset
// if (input != null) {
// for (String ms : input.trim().split(" ")) {
// ModifierSet temp = new ModifierSet();
// temp.temp = ms;
// results.add(temp);
// }
// }
// return results;
// // Set<ModifierSet> 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<ModifierSet> input) {
// return input.toString();
// }
// }
public static Set<String> getPlatformIDs() {
Set<String> results = new LinkedHashSet<String>();
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<String> getKeyboardIDs(String platformId) {
Set<String> results = new LinkedHashSet<String>();
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<String> names,
Fallback fallback, Set<KeyMap> keyMaps, Map<TransformType, Transforms> 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<Exception> 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<Exception> 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<Exception> 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<String, Iso> hardwareMap;
public String getId() {
return id;
}
public Map<String, Iso> getHardwareMap() {
return hardwareMap;
}
public Platform(String id, Map<String, Iso> 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<Gesture, List<String>> gestures;
public Output(String output, Map<Gesture, List<String>> 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<Gesture, List<String>> getGestures() {
return gestures;
}
public String toString() {
return "{" + output + "," + transformStatus + ", " + gestures + "}";
}
}
public static class KeyMap {
private final KeyboardModifierSet modifiers;
final Map<Iso, Output> iso2output;
public KeyMap(KeyboardModifierSet keyMapModifiers, Map<Iso, Output> data) {
this.modifiers = keyMapModifiers;
this.iso2output = Collections.unmodifiableMap(data);
}
public KeyboardModifierSet getModifiers() {
return modifiers;
}
public Map<Iso, Output> getIso2Output() {
return iso2output;
}
public String toString() {
return "{" + modifiers + "," + iso2output + "}";
}
}
public static class Transforms {
final Map<String, String> string2string;
public Transforms(Map<String, String> data) {
this.string2string = data;
}
public Map<String, String> getMatch(String prefix) {
Map<String, String> results = new LinkedHashMap<String, String>();
for (Entry<String, String> 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<String> names;
private final Set<KeyMap> keyMaps;
private final Map<TransformType, Transforms> transforms;
public String getLocaleId() {
return locale;
}
public String getVersion() {
return version;
}
public String getPlatformVersion() {
return platformVersion;
}
public Fallback getFallback() {
return fallback;
}
public Set<String> getNames() {
return names;
}
public Set<KeyMap> getKeyMaps() {
return keyMaps;
}
public Map<TransformType, Transforms> 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<Output> values, UnicodeSet results) {
for (Output value : values) {
if (value.output != null && !value.output.isEmpty()) {
results.add(value.output);
}
for (List<String> outputList : value.gestures.values()) {
results.addAll(outputList);
}
}
}
private static class PlatformHandler extends SimpleHandler {
String id;
Map<String, Iso> hardwareMap = new HashMap<String, Iso>();
XPathParts parts = new XPathParts();
public void handlePathValue(String path, String value) {
parts.set(path);
// <platform id='android'/>
id = parts.getAttributeValue(0, "id");
if (parts.size() > 1) {
String element1 = parts.getElement(1);
// <platform> <hardwareMap> <map keycode='0' iso='C01'/>
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<Exception> errors; // = new LinkedHashSet<Exception>();
Set<String> errors2 = new LinkedHashSet<String>();
// doesn't do any error checking for collisions, etc. yet.
String locale; // TODO
String version; // TODO
String platformVersion; // TODO
Set<String> names = new LinkedHashSet<String>();
Fallback fallback = Fallback.BASE;
KeyboardModifierSet keyMapModifiers = null;
Map<Iso, Output> iso2output = new EnumMap<Iso, Output>(Iso.class);
Set<KeyMap> keyMaps = new LinkedHashSet<KeyMap>();
TransformType currentType = null;
Map<String, String> currentTransforms = null;
Map<TransformType, Transforms> transformMap = new EnumMap<TransformType, Transforms>(TransformType.class);
XPathParts parts = new XPathParts();
LanguageTagParser ltp = new LanguageTagParser();
public KeyboardHandler(Set<Exception> errorsOutput) {
errors = errorsOutput;
errors.clear();
}
public Keyboard getKeyboard() {
// finish everything off
addToKeyMaps();
if (currentType != null) {
transformMap.put(currentType, new Transforms(currentTransforms));
}
// errors.clear();
// errors.addAll(this.errors);
return new Keyboard(locale, version, platformVersion, names, fallback, keyMaps, transformMap);
}
public void handlePathValue(String path, String value) {
// System.out.println(path);
try {
parts.set(path);
if (locale == null) {
// <keyboard locale='bg-t-k0-chromeos-phonetic'>
locale = parts.getAttributeValue(0, "locale");
ltp.set(locale);
Map<String, String> extensions = ltp.getExtensions();
LanguageTagParser.Status status = ltp.getStatus(errors2);
if (errors2.size() != 0 || !extensions.containsKey("t")) {
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")) {
// <baseMap fallback='true'>/ <map iso="E00" chars="Ù€"/> <!-- ` -->
Iso iso = Iso.valueOf(parts.getAttributeValue(2, "iso"));
if (DEBUG) {
System.out.println("baseMap: iso=" + iso + ";");
}
final Output output = getOutput();
if (output != null) {
iso2output.put(iso, output);
}
} else if (element1.equals("keyMap")) {
// <keyMap modifiers='shift+caps?'><map base="Ù¡" chars="!"/> <!-- 1 -->
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<Iso, Output>();
keyMapModifiers = newMods;
}
String isoString = parts.getAttributeValue(2, "iso");
if (DEBUG) {
System.out.println("keyMap: base=" + isoString + ";");
}
final Output output = getOutput();
if (output != null) {
iso2output.put(Iso.valueOf(isoString), output);
}
} else if (element1.equals("transforms")) {
// <transforms type='simple'> <transform from="` " to="`"/>
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<String, String>();
}
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")) {
// <version platform='0.17' number='$Revision$'/>
platformVersion = parts.getAttributeValue(1, "platform");
version = parts.getAttributeValue(1, "number");
} else if (element1.equals("names")) {
// <names> <name value='cs'/>
names.add(parts.getAttributeValue(2, "value"));
} else if (element1.equals("settings")) {
// <settings fallback='omit'/>
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() {
String chars = null;
TransformStatus transformStatus = TransformStatus.DEFAULT;
Map<Gesture, List<String>> gestures = new EnumMap<Gesture, List<String>>(Gesture.class);
for (Entry<String, String> 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<String> list = new LinkedHashSet<String>();
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<String>(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);
}
}
}