/*
* $Id$
*
* Copyright (C) 2003-2015 JNode.org
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.driver.input;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.TreeSet;
import org.apache.log4j.Logger;
import org.jnode.plugin.ConfigurationElement;
import org.jnode.plugin.Extension;
import org.jnode.plugin.ExtensionPoint;
import org.jnode.plugin.ExtensionPointListener;
/**
* The KeyboardManager provides methods for creating KeyboardInterpreter objects, and managing
* the mapping from various kinds of identifiers to keyboard layout classes.
*
* @author Marc DENTY
* @author crawley@jnode.org
*/
public class KeyboardLayoutManager implements ExtensionPointListener {
private final Logger log = Logger.getLogger(KeyboardLayoutManager.class);
/**
* The name used to bind this manager in the InitialNaming namespace.
*/
public static Class<KeyboardLayoutManager> NAME = KeyboardLayoutManager.class;
public static final String EP_NAME = "org.jnode.driver.input.keyboard-layouts";
private final HashMap<String, KeyboardInterpreter.Factory> map =
new HashMap<String, KeyboardInterpreter.Factory>();
private final ExtensionPoint keyboardLayoutEP;
public KeyboardLayoutManager(ExtensionPoint keyboardLayoutEP) {
this.keyboardLayoutEP = keyboardLayoutEP;
keyboardLayoutEP.addListener(this);
for (Extension extension : keyboardLayoutEP.getExtensions()) {
addLayoutsToMap(extension);
}
}
/**
* Load the default keyboard layout as specified in the 'org.jnode.driver.input.KeyboardLayout'
* resource bundle. If none is specified or the specified layout cannot be used, we use the
* 'US_en' layout as a fall-back.
*
* @return a valid KeyboardInterpreter
* @throws KeyboardInterpreterException
*/
public KeyboardInterpreter createDefaultKeyboardInterpreter() throws KeyboardInterpreterException {
String country = null;
String region = null;
String variant = null;
try {
ResourceBundle rb = ResourceBundle.getBundle("org.jnode.driver.input.KeyboardLayout",
Locale.getDefault(), Thread.currentThread().getContextClassLoader());
country = getProperty(rb, "defaultCountry");
// FIXME the following property name should be 'defaultLanguage'
region = getProperty(rb, "defaultRegion");
variant = getProperty(rb, "defaultVariant");
} catch (Exception ex) {
log.error("Cannot find the 'org.jnode.driver.input.KeyboardLayout' resource bundle", ex);
}
if (country != null) {
try {
KeyboardInterpreter ki = createKeyboardInterpreter(country, region, variant);
if (ki != null) {
return ki;
}
} catch (Exception ex) {
log.error("Cannot load the default '" +
makeKeyboardInterpreterID(country, region, variant) +
"' keyboard interpreter", ex);
}
}
// Use the US_en keyboard layout as a fall-back if there was no resource bundle, no
// usable default keyboard layout or the specified default layout had no interpreter.
log.error("Trying the 'US_en' keyboard interpreter as a fallback");
try {
return createKeyboardInterpreter("US_en");
} catch (Throwable ex) {
log.error("Cannot load 'US_en' keyboard interpreter", ex);
throw new KeyboardInterpreterException("Cannot create a keyboard interpreter", ex);
}
}
/**
* Get a String-valued property value from the resource bundle, dealing
* with empty and missing values.
*
* @param rb the resource bundle
* @param key the property name
* @return the property value or <code>null</null> if the value is missing or empty.
*/
private String getProperty(ResourceBundle rb, String key) {
try {
String res = rb.getString(key);
res = res.trim();
return (res.length() == 0) ? null : res;
} catch (RuntimeException e) { /* ClassCastException or MissingResourceException */
return null;
}
}
/**
* Create a new keyboard interpreter object. Note that keyboard interpreter
* objects are stateful and therefore cannot be shared by multiple keyboards.
*
* @param id a keyboard layout name or identifier.
* @return a KeyboardInterpreter
* @throws KeyboardInterpreterException
*/
public KeyboardInterpreter createKeyboardInterpreter(String id)
throws KeyboardInterpreterException {
log.debug("Looking for interpreter for keyboard layout '" + id + '\'');
KeyboardInterpreter.Factory factory = null;
synchronized (this) {
factory = map.get(id);
}
if (factory != null) {
return factory.create();
} else {
// As a fall-back look for a hard-coded keyboard interpreter class in the
// org.jnode.driver.input.l10n plugin.
final String classI10N = "org.jnode.driver.input.l10n.KeyboardInterpreter_" + id;
try {
return new KIClassWrapper(classI10N).create();
} catch (MissingKeyboardInterpreterClassException ex) {
// We tried and failed the fall-back, so report original problem:
// that 'id' is not a registered keyboard interpreter.
throw new KeyboardInterpreterException(
"No keyboard interpreter registered with id '" + id + '\'');
}
}
}
/**
* Create a new keyboard interpreter object. Note that keyboard interpreter
* objects are stateful and therefore cannot be shared by multiple keyboards.
*
* @param country the country code; e.g. US, GB, FR, DE, etc.
* @param language the language code; e.g. en, fr, de etc or <code>null</code>
* @param variant a keyboard variant name or <code>null</code>.
* @return a KeyboardInterpreter
* @throws KeyboardInterpreterException
*/
public KeyboardInterpreter createKeyboardInterpreter(
String country, String language, String variant)
throws KeyboardInterpreterException {
return createKeyboardInterpreter(
makeKeyboardInterpreterID(country, language, variant));
}
/**
* Convert a country / language / variant keyboard triple into a keyboard
* layout identifier.
*
* @param country the country code; e.g. US, GB, FR, DE, etc.
* @param language the language code; e.g. en, fr, de etc or <code>null</code>.
* @param variant a keyboard variant name or <code>null</code>.
* @return the keyboard layout identifier.
*/
public String makeKeyboardInterpreterID(
String country, String language, String variant) {
final String id;
country = country.toUpperCase();
if (language != null) {
language = language.toLowerCase();
if (variant == null) {
id = country + '_' + language;
} else {
id = country + '_' + language + '_' + variant;
}
} else if (variant != null) {
id = country + '_' + variant;
} else {
id = country;
}
return id;
}
/**
* Register a keyboard interpreter factory object. This factory could be an
* instance of {@link KIClassWrapper} or some other factory.
*
* @param name the keyboard layout identifier.
* @param factory the factory to be registered.
*/
public synchronized void add(String name, KeyboardInterpreter.Factory factory) {
map.put(name, factory);
}
/**
* Register a keyboard interpreter class. The classname will be wrapped in
* a new KeyboardInterpreter.Factory.
*
* @param name the keyboard layout identifier.
* @param className the name of the class to be registered.
*/
public void add(String name, String className) {
add(name, new KIClassWrapper(className));
}
/**
* Remove the keyboard interpreter factory for a given layout identifier.
* @param name keyboard layout identifier.
* @return the factory removed or <code>null</node>
*/
public synchronized KeyboardInterpreter.Factory remove(String name) {
return map.remove(name);
}
/**
* Gets a collection of all layout identifiers known to this manager.
*/
public synchronized Collection<String> layouts() {
return new TreeSet<String>(map.keySet());
}
/**
* Gets an iterator for the layout identifiers known to this manager.
*
* @return An iterator the returns instances of String.
*/
public Iterator<String> layoutIterator() {
return layouts().iterator();
}
/**
* Add all keyboard layouts in the extension data to the map.
*/
@Override
public void extensionAdded(ExtensionPoint point, Extension extension) {
if (point.equals(keyboardLayoutEP)) {
addLayoutsToMap(extension);
}
}
/**
* Remove from the map any keyboard layouts that are identical to the extension data.
*/
@Override
public void extensionRemoved(ExtensionPoint point, Extension extension) {
if (point.equals(keyboardLayoutEP)) {
removeLayoutsFromMap(extension);
}
}
private synchronized void addLayoutsToMap(Extension extension) {
for (ConfigurationElement element : extension.getConfigurationElements()) {
if (element.getName().equals("layout")) {
String name = element.getAttribute("name");
String className = element.getAttribute("class");
if (name != null && className != null) {
add(name, className);
}
}
}
}
private synchronized void removeLayoutsFromMap(Extension extension) {
for (ConfigurationElement element : extension.getConfigurationElements()) {
if (element.getName().equals("layout")) {
String name = element.getAttribute("name");
String className = element.getAttribute("class");
if (name == null || className == null) {
continue;
}
KeyboardInterpreter.Factory factory = map.get(name);
if (factory != null && (factory instanceof KIClassWrapper) &&
((KIClassWrapper) factory).className.equals(className)) {
map.remove(name);
}
}
}
}
/**
* This wrapper class allows us to treat a class name as a keyboard interpreter
* factory.
*/
public static class KIClassWrapper implements KeyboardInterpreter.Factory {
private final String className;
/**
* Create a wrapper object for a class.
*
* @param className the FQN of a class that should implement the KeyboardInterpreter
* interface and provide a public no-args constructor.
*/
public KIClassWrapper(String className) {
this.className = className;
}
/**
* Create an instance corresponding to the wrapped class name.
*
* @throws MissingKeyboardInterpreterClassException if the class was not found
* @throws KeyboardInterpreterException for some other error; e.g. if there is
* not a public no-args constructor, if an exception is thrown by the constructor
* or if the resulting object is not a KeyboardInterpreter.
*/
@Override
public KeyboardInterpreter create() throws KeyboardInterpreterException {
try {
// FIXME ... think about whether using the current thread's class
// loader might present a security issue.
final ClassLoader cl = Thread.currentThread().getContextClassLoader();
return (KeyboardInterpreter) cl.loadClass(className).newInstance();
} catch (ClassNotFoundException ex) {
throw new MissingKeyboardInterpreterClassException(
"Keyboard interpreter class not found: " + className);
} catch (Exception ex) {
// Could be an access, and instantiation or a typecast exception ...
throw new KeyboardInterpreterException(
"Error instantiating keyboard interpreter class:" +
className, ex);
}
}
@Override
public String describe() {
return className;
}
}
}