/*******************************************************************************
* Copyright (c) 2004, 2010 BREDEX GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BREDEX GmbH - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.jubula.rc.common.driver;
import java.awt.AWTEvent;
import java.awt.AWTException;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import javax.swing.KeyStroke;
import org.apache.commons.lang.Validate;
import org.eclipse.jubula.rc.common.exception.RobotException;
import org.eclipse.jubula.rc.common.exception.StepExecutionException;
import org.eclipse.jubula.rc.common.logger.AutServerLogger;
import org.eclipse.jubula.rc.common.util.MatchUtil;
import org.eclipse.jubula.tools.internal.objects.event.EventFactory;
import org.eclipse.jubula.tools.internal.objects.event.TestErrorEvent;
import org.eclipse.jubula.tools.internal.utils.EnvironmentUtils;
/**
* Presses keys. Supports "native" keypresses, in the sense that one can
* optionally choose not to wait for event confirmation.
*
* @author BREDEX GmbH
* @created Oct 24, 2007
*/
public class KeyTyper {
/** Key codes for keys that also appear on the number pad */
public static final int [] NUMPAD_KEYCODES = {
KeyEvent.VK_PAGE_UP,
KeyEvent.VK_PAGE_DOWN,
KeyEvent.VK_HOME,
KeyEvent.VK_END,
KeyEvent.VK_DELETE,
KeyEvent.VK_INSERT,
KeyEvent.VK_RIGHT,
KeyEvent.VK_LEFT,
KeyEvent.VK_UP,
KeyEvent.VK_DOWN,
KeyEvent.VK_KP_RIGHT,
KeyEvent.VK_KP_LEFT,
KeyEvent.VK_KP_UP,
KeyEvent.VK_KP_DOWN
};
/** regexp specifying the legal formatting of native input strings */
private static final String VALID_INPUT = "[a-zA-z0-9]*"; //$NON-NLS-1$
/** the logger */
private static AutServerLogger log =
new AutServerLogger(KeyTyper.class);
/** single instance */
private static KeyTyper instance = null;
/** robot used for native key presses */
private Robot m_robot;
/**
* Private constructor
*
* @throws AWTException if there is a problem creating the Robot.
*/
private KeyTyper() throws AWTException {
m_robot = new Robot();
}
/**
*
* @return the single instance.
*/
public static KeyTyper getInstance() throws AWTException {
if (instance == null) {
instance = new KeyTyper();
}
return instance;
}
/**
* Types the given keystroke.
* If any of the intercepting and event matching arguments are
* <code>null</code>, this method will not wait for event confirmation. It
* will simply assume that the events were received correctly. Otherwise,
* this method will use the given interceptor and event matcher arguments to
* handle event confirmation.
*
* @param keyStroke The key stroke. May not be null.
* @param interceptor The interceptor that will be used to wait for event
* confirmation.
* @param keyDownMatcher The event matcher to be used for key press event
* confirmation.
* @param keyUpMatcher The event matcher to be used for key release event
* confirmation.
*/
public void type(KeyStroke keyStroke, IRobotEventInterceptor interceptor,
IEventMatcher keyDownMatcher, IEventMatcher keyUpMatcher) {
try {
Validate.notNull(keyStroke);
boolean waitForConfirm = interceptor != null
&& keyDownMatcher != null && keyUpMatcher != null;
InterceptorOptions options = new InterceptorOptions(new long[]{
AWTEvent.KEY_EVENT_MASK});
List<Integer> keycodes = modifierKeyCodes(keyStroke);
keycodes.add(new Integer(keyStroke.getKeyCode()));
if (log.isDebugEnabled()) {
String keyModifierText = KeyEvent.getKeyModifiersText(keyStroke
.getModifiers());
String keyText = KeyEvent.getKeyText(keyStroke.getKeyCode());
log.debug("Key stroke: " + keyStroke); //$NON-NLS-1$
log.debug("Modifiers, Key: " + keyModifierText + ", " + keyText); //$NON-NLS-1$//$NON-NLS-2$
log.debug("number of keycodes: " + keycodes.size()); //$NON-NLS-1$
}
m_robot.setAutoWaitForIdle(true);
// FIXME Hack for MS Windows for keys that also appear on the numpad.
// Turns NumLock off. Does nothing if locking key functionality
// isn't implemented for the operating system.
boolean isNumLockToggled = hackWindowsNumpadKeys1(
keyStroke.getKeyCode());
// first press all keys, then release all keys, but
// avoid to press and release any key twice (even if perhaps alt
// and meta should have the same keycode(??)
Set<Integer> alreadyDown = new HashSet<Integer>();
ListIterator<Integer> i = keycodes.listIterator();
try {
while (i.hasNext()) {
Integer keycode = i.next();
if (log.isDebugEnabled()) {
log.debug("trying to press: " + keycode); //$NON-NLS-1$
}
if (!alreadyDown.contains(keycode)) {
IRobotEventConfirmer confirmer = null;
if (waitForConfirm) {
confirmer = interceptor.intercept(options);
}
if (log.isDebugEnabled()) {
log.debug("pressing: " + keycode); //$NON-NLS-1$
}
alreadyDown.add(keycode);
m_robot.keyPress(keycode.intValue());
if (waitForConfirm) {
confirmer.waitToConfirm(null, keyDownMatcher);
}
}
}
} finally {
releaseKeys(options, alreadyDown, i, interceptor, keyUpMatcher);
// FIXME Hack for MS Windows for keys that also appear on the numpad.
// Turns NumLock back on, if necessary.
if (isNumLockToggled) {
hackWindowsNumpadKeys2();
}
}
} catch (IllegalArgumentException e) {
throw new RobotException(e);
}
}
/**
* Types the given keystroke. The arguments must adhere to the specification
* at <a
* href=http://java.sun.com/j2se/1.4.2/docs/api/javax/swing/KeyStroke.html#getKeyStroke(java.lang.String)>
* If any of the intercepting and event matching arguments are
* <code>null</code>, this method will not wait for event confirmation. It
* will simply assume that the events were received correctly. Otherwise,
* this method will use the given interceptor and event matcher arguments to
* handle event confirmation.
*
* @param keyStrokeSpec The key code.
* @param interceptor The interceptor that will be used to wait for event
* confirmation.
* @param keyDownMatcher The event matcher to be used for key press event
* confirmation.
* @param keyUpMatcher The event matcher to be used for key release event
* confirmation.
*/
public void type(String keyStrokeSpec, IRobotEventInterceptor interceptor,
IEventMatcher keyDownMatcher, IEventMatcher keyUpMatcher) {
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyStrokeSpec);
if (keyStroke == null) {
String trimmedKeyStrokeSpec = keyStrokeSpec.trim();
int indexOfKeySpec = trimmedKeyStrokeSpec.lastIndexOf(" "); //$NON-NLS-1$
if (indexOfKeySpec != -1) {
String keySpec = trimmedKeyStrokeSpec.substring(
indexOfKeySpec + 1,
trimmedKeyStrokeSpec.length());
KeyStroke checkKeyStroke = KeyStroke.getKeyStroke(keySpec);
if (checkKeyStroke == null) {
throw new StepExecutionException(
"invalid key spec", //$NON-NLS-1$
EventFactory.createActionError(
"TestErrorEvent.InvalidKeySpec", //$NON-NLS-1$
new String [] {keySpec}));
}
String invalidModifier = trimmedKeyStrokeSpec.substring(0,
indexOfKeySpec).trim();
throw new StepExecutionException(
"invalid modifier", //$NON-NLS-1$
EventFactory.createActionError(
"TestErrorEvent.InvalidModifier", //$NON-NLS-1$
new String [] {invalidModifier}));
}
}
type(keyStroke, interceptor, keyDownMatcher, keyUpMatcher);
}
/**
* @param keyStroke KeyStroke whose modifiers are requested
* @return a List of KeyCodes (hopefully) realizing the ModifierMask contained in the KeyStroke
*/
private List<Integer> modifierKeyCodes(KeyStroke keyStroke) {
List<Integer> l = new LinkedList<Integer>();
int modifiers = keyStroke.getModifiers();
// this is jdk 1.3 - code.
// use ALT_DOWN_MASK instead etc. with jdk 1.4 !
if ((modifiers & InputEvent.ALT_MASK) != 0) {
l.add(new Integer(KeyEvent.VK_ALT));
}
if ((modifiers & InputEvent.ALT_GRAPH_MASK) != 0) {
l.add(new Integer(KeyEvent.VK_ALT_GRAPH));
}
if ((modifiers & InputEvent.CTRL_MASK) != 0) {
l.add(new Integer(KeyEvent.VK_CONTROL));
}
if ((modifiers & InputEvent.SHIFT_MASK) != 0) {
l.add(new Integer(KeyEvent.VK_SHIFT));
}
if ((modifiers & InputEvent.META_MASK) != 0) {
l.add(new Integer(KeyEvent.VK_META));
}
return l;
}
/**
* Fix for MS Windows for keys that also appear on the numpad. Turns
* NumLock off if it is on.
* First method called of a two-part fix.
* @param keyCode keycode to check
* @return <code>True</code>, if the NumLock status was toggled. Otherwise
* <code>false</code>. Basically, a value of true indicates that
* second part of this fix must also be used.
*/
private boolean hackWindowsNumpadKeys1(int keyCode) {
if (!EnvironmentUtils.isWindowsOS()) {
return false;
}
// FIXME Fix for MS Windows for keys that also appear on the numpad.
// Turns NumLock off.
boolean isNumpadKey = false;
for (int i = 0; i < NUMPAD_KEYCODES.length; ++i) {
if (NUMPAD_KEYCODES[i] == keyCode) {
isNumpadKey = true;
break;
}
}
boolean wasNumLockToggled = false;
if (isNumpadKey) {
try {
// FIXME Extra-ugly hack to get the CURRENT status of
// NumLock.
/*
* See:
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6446890
* Using NumLock as the key to press because it seems
* least likely to affect the AUT. We also type it twice,
* obviously, so as not to change the real status of NumLock.
*/
m_robot.keyPress(KeyEvent.VK_NUM_LOCK);
m_robot.keyRelease(KeyEvent.VK_NUM_LOCK);
m_robot.keyPress(KeyEvent.VK_NUM_LOCK);
m_robot.keyRelease(KeyEvent.VK_NUM_LOCK);
final Toolkit defaultToolkit = Toolkit.getDefaultToolkit();
if (defaultToolkit.getLockingKeyState(KeyEvent.VK_NUM_LOCK)) {
defaultToolkit.setLockingKeyState(KeyEvent.VK_NUM_LOCK,
false);
wasNumLockToggled = true;
}
} catch (UnsupportedOperationException usoe) {
// OS does not support locking key operations
// Do nothing -> Leave NumLock alone
log.info("UnsupportedOperationException thrown by NumPad " //$NON-NLS-1$
+ "workaround. NumLock will not be toggled."); //$NON-NLS-1$
}
}
return wasNumLockToggled;
}
/**
* Fix for MS Windows for keys that also appear on the numpad. Turns
* NumLock on.
* Second method called of a two-part fix.
*/
private void hackWindowsNumpadKeys2() {
Toolkit.getDefaultToolkit().setLockingKeyState(
KeyEvent.VK_NUM_LOCK, true);
}
/**
* @param options options
* @param alreadyDown alreadyDown
* @param i i
* @param interceptor The interceptor that will be used to wait for event
* confirmation.
* @param keyUpMatcher The event matcher to be used for key release event
* confirmation.
*/
private void releaseKeys(InterceptorOptions options,
Set<Integer> alreadyDown, ListIterator<Integer> i,
IRobotEventInterceptor interceptor, IEventMatcher keyUpMatcher) {
boolean waitForConfirm = interceptor != null && keyUpMatcher != null;
// Release all keys in reverse order.
Set<Integer> alreadyUp = new HashSet<Integer>();
while (i.hasPrevious()) {
Integer keycode = i.previous();
if (log.isDebugEnabled()) {
log.debug("trying to release: " + keycode.intValue()); //$NON-NLS-1$
}
if (!alreadyUp.contains(keycode)
&& alreadyDown.contains(keycode)) {
try {
IRobotEventConfirmer confirmer = null;
if (waitForConfirm) {
confirmer = interceptor.intercept(options);
}
if (log.isDebugEnabled()) {
log.debug("releasing: " + keycode.intValue()); //$NON-NLS-1$
}
alreadyUp.add(keycode);
m_robot.keyRelease(keycode.intValue());
if (waitForConfirm) {
confirmer.waitToConfirm(null, keyUpMatcher);
}
} catch (RobotException e) {
log.error("error releasing keys", e); //$NON-NLS-1$
if (!i.hasPrevious()) {
throw e;
}
}
}
}
}
/**
* Types the given string without checking for event confirmation. Note that
* that only alphanumeric characters can be typed using this method.
*
* @param text The text to type.
*/
public void nativeTypeString(String text) {
// Verify that the string consists of only valid characters
// (any change in verification will probably require a change in
// how the text is processed)
if (MatchUtil.getInstance().match(
text, VALID_INPUT, MatchUtil.MATCHES_REGEXP)) {
boolean isCapsLockOn = false;
try {
isCapsLockOn = Toolkit.getDefaultToolkit().getLockingKeyState(
KeyEvent.VK_CAPS_LOCK);
} catch (UnsupportedOperationException uoe) {
// Do nothing.
// Querying the status of the Caps Lock key is not possible on
// certain platforms (ex. Linux). In this case, we will just
// assume that Caps Lock is not active.
}
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
boolean holdShift = Character.isUpperCase(c)
^ isCapsLockOn;
StringBuffer sb = new StringBuffer();
if (holdShift) {
sb.append("shift "); //$NON-NLS-1$
}
sb.append(Character.toUpperCase(c));
type(sb.toString(), null, null, null);
}
} else {
throw new StepExecutionException(
"Invalid input string (only ASCII alphanumeric strings are allowed)", //$NON-NLS-1$
EventFactory.createActionError(
TestErrorEvent.INVALID_PARAM_VALUE));
}
}
}