* Constructor. Creates a StructureNode of type 'Recordable' with a valid id and builds its
* Jacareto XML representation.
*
*/
public StructureNode() {
content = "();
}
/**
*
* Builds the XML representation of a Jacareto structure type.
*
*
* @param type
* the type of this StructureNode, for example: 'MouseDownEvent'
*/
public void setContent(String type) {
content = "";
}
/**
*
* Adds a new StructureNode as a child of this node.
*
*
* @param type
* the type of the child node, for example: 'MouseDownEvent'
*/
public StructureNode add(String type) {
StructureNode node = new StructureNode(type);
children.add(node);
return node;
}
/**
*
* Builds the XML representation of a Jacareto structure type.
*
*
* @param type
* the type of this StructureNode, for example: 'MouseDownEvent'
*/
public void addRecordable() {
children.add(new StructureNode());
}
/**
*
* Returns a Jacareto XML representation of this StructureNode, includin all its children.
*
*/
@Override
public String toString() {
String separator = System.getProperty("line.separator");
StringBuffer result = new StringBuffer(content);
result.append(separator);
for (StructureNode child : children) {
result.append(child.toString());
}
if (content.endsWith("/>")) {
return result.toString();
}
result.append("");
result.append(separator);
return result.toString();
}
}
/**
*
* The time it takes for Jacareto to replay an event in ms.
*
*/
private static final int EVENT_DURATION = 150;
/**
*
* The time it takes for Jacareto to replay each part of a double click event in ms.
*
*/
private static final int DOUBLE_CLICK_DURATION = 50;
/**
*
* Application startup time in ms. The application needs to be fully initialized before Jacareto
* can start replaying.
*
*/
private static final int STARTUP_DELAY = 10000;
/**
*
* The GUI element which is currently focused.
*
*/
private JFCGUIElement currentFocus;
/**
*
* A tree of StructureNodes which represents the structure part inside a Jacareto XML file.
*
*/
private StructureNode structure;
/**
*
* XML structure for key events modeled as StructureNodes.
*
*/
private StructureNode lastKeySequenceEvent;
/**
*
* XML structure for key events modeled as StructureNodes.
*
*/
private StructureNode lastKeyTypedEvent;
/**
*
* Bitmask which represents currently used key modifiers (such as shift etc).
*
*/
private int currentKeyModifiers;
/**
*
* Maps VirtualKey objects for modifier keys back to AWT Event codes.
*
*/
private HashMap modifiers;
/**
*
* XML structure for mouse events modeled as StructureNodes.
*
*/
private StructureNode lastMouseClickEvent;
/**
*
* XML structure for focus events modeled as StructureNodes.
*
*/
private StructureNode lastFocusChangeEvent;
/**
*
* XML structure for item and action events modeled as StructureNodes.
*
*/
private StructureNode lastItemActionEvent;
/**
*
* The target of the last mouseDownEvent. It is necessary to save this because mouse down and up
* targets can differ.
*
*/
private IEventTarget lastMouseDownTarget;
/**
*
* Associates the name of a menu element with its corresponding JFCGUIElement.
*
*/
private HashMap menuElements;
/**
*
* The menu hierarchy.
*
*/
private List menuList;
/**
*
* Autoquest input sequence.
*
*/
private List sequence;
/**
*
* XML output filename.
*
*/
private String filename;
/**
*
* Jacareto replay execution classpath.
*
*/
private String classpath;
/**
*
* Jacareto replay execution basepath.
*
*/
private String basepath;
/**
*
* Jacareto replay execution classpathext.
*
*/
private String classpathext;
/**
*
* Constructor.
*
*/
public JFCJacaretoReplayGenerator(List sequence,
String filename,
String classpath,
String basepath,
String classpathext,
String menu)
{
this.sequence = sequence;
this.filename = filename;
this.classpath = classpath;
this.basepath = basepath;
this.classpathext = classpathext;
currentFocus = null;
currentKeyModifiers = 0;
StructureNode.nextRef = 0;
lastFocusChangeEvent = null;
lastItemActionEvent = null;
lastKeySequenceEvent = null;
lastKeyTypedEvent = null;
lastMouseClickEvent = null;
lastMouseDownTarget = null;
menuElements = new HashMap<>();
modifiers = createModifierMap();
// try to parse in menu file, if available
if (!menu.isEmpty()) {
try {
menuList = Files.readAllLines(Paths.get(menu), Charset.defaultCharset());
}
catch (IOException e) {
Console.printerrln("Unable to open menu file");
Console.logException(e);
}
}
}
/**
*
* Writes the Jacareto XML replay file.
*
*/
public void writeJacaretoXML() {
BufferedWriter writer = new BufferedWriter(openReplayFile(filename + ".xml"));
try {
writeJacaretoHead(writer, classpath, basepath, classpathext);
writeJacaretoEvents(writer, sequence);
writeJacaretoTail(writer);
writeLine(writer, "");
writer.flush();
writer.close();
}
catch (IOException e) {
Console.printerrln("Unable to write Jacareto replay file " + filename);
}
}
/**
*
* Associates keyboard modifier keys with their AWT event codes.
*
*/
private HashMap createModifierMap() {
HashMap result = new HashMap<>();
result.put(VirtualKey.SHIFT, 1);
result.put(VirtualKey.CONTROL, 2);
result.put(VirtualKey.ALT, 8);
result.put(VirtualKey.ALT_GRAPH, 32);
return result;
}
/**
*
* Writes a line and creates a new line.
*
*
* @param writer
* the BufferedWriter which writes the XML
* @param line
* the line to write
*
*/
private void writeLine(BufferedWriter writer, String line) throws IOException {
writer.write(line);
writer.newLine();
}
/**
*
* Writes the Jacareto XML head part. This mainly contains information about the state of the
* system when the replay was captured.
*
* @param writer
* the BufferedWriter which writes the XML
* @param classname
* name of the main class of the program that will be replayed
* @param basepath
* a basepath that is prepended to all paths specified in classpathext
* @param classpathext
* additional required resources (e.g. jar files)
*
*
*/
private void writeJacaretoHead(BufferedWriter writer,
String classname,
String basepath,
String classpathext) throws IOException
{
Calendar now = Calendar.getInstance();
writeLine(writer, "");
writeLine(writer, "");
writeLine(writer, "");
//@formatter:off
writeLine(writer, ""
);
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
writeLine(writer, ""
);
writeLine(writer,
"");
writeLine(writer, "");
writeLine(writer, ""
);
//@formatter:on
}
/**
*
* Writes Jacareto XML code for all events within the Autoquest sequences.
*
*
* @param writer
* the BufferedWriter which writes the XML
* @param sequences
* the Autoquest sequences
*
*/
private void writeJacaretoEvents(BufferedWriter writer, List sequence)
throws IOException
{
structure = new StructureNode("RootElement");
// reference the elements that we included in the header
structure.addRecordable(); // Calendar
structure.addRecordable(); // SystemInfo
structure.addRecordable(); // KeyboardState
structure.addRecordable(); // ComponentMode
structure.addRecordable(); // ApplicationStarter
for (Iterator eventIter = sequence.iterator(); eventIter.hasNext();) {
Event event = eventIter.next();
if (event.getType() instanceof MouseButtonDown) {
handleMouseButtonDown(writer, event);
}
else if (event.getType() instanceof MouseButtonUp) {
handleMouseButtonUp(writer, event);
}
else if (event.getType() instanceof MouseDoubleClick) {
handleMouseDoubleClick(writer, event);
}
else if (event.getType() instanceof MouseClick) {
if (event.getTarget() instanceof JFCMenuButton) {
// if a menu file was provided, use the improved event
// generation
if (menuList != null) {
if (menuElements.isEmpty()) {
// parse the menu structure
GUIModel model = ((IGUIElement) event.getTarget()).getGUIModel();
getMenuElements(model.getRootElements(), model);
}
Stack hierarchy =
findMenuItemHierarchy((JFCGUIElement) event.getTarget());
while (!hierarchy.empty()) {
generateFullClick(writer, event, hierarchy.pop());
}
continue;
}
}
handleMouseClick(writer, event);
}
else if (event.getType() instanceof KeyboardFocusChange) {
handleKeyboardFocusChange(writer, event);
}
else if (event.getType() instanceof MouseDragAndDrop) {
handleMouseDragAndDrop(writer, event);
}
else if (event.getType() instanceof KeyPressed) {
handleKeyPressed(writer, event);
}
else if (event.getType() instanceof KeyReleased) {
handleKeyReleased(writer, event);
}
else if (event.getType() instanceof TextInput) {
handleTextInput(writer, event);
}
else {
Console.traceln(Level.WARNING, "No handler for event \"" + event + "\". Skipped.");
}
}
}
// EVENT HANDLERS
private void handleMouseClick(BufferedWriter writer, Event event) throws IOException {
lastKeySequenceEvent = null;
if (lastMouseClickEvent != null) {
if (lastMouseDownTarget == event.getTarget()) {
// this is the standard case:
// mouseDown, mouseUp and mouseClick sequence
// was triggered on this target
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(),
EVENT_DURATION, 500);
writeItemActionEvent(writer, event);
if (lastFocusChangeEvent == null) {
// write structure sequentially
structure.children.add(lastMouseClickEvent);
structure.children.add(lastItemActionEvent);
lastMouseClickEvent = null;
}
else {
// with nested structure
structure.children.add(lastItemActionEvent);
lastItemActionEvent.children.add(0, lastFocusChangeEvent);
lastFocusChangeEvent.children.add(0, lastMouseClickEvent);
lastFocusChangeEvent = null;
lastMouseClickEvent = null;
}
}
else {
// target of mouseDown and mouseClick are different
// -> this is, for example, a click on a menu item
// within a condensed sequence
commitFocusEvent();
// finish the last click on the old target
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(),
EVENT_DURATION, 500);
structure.children.add(lastMouseClickEvent);
// and generate a new one
generateFullClick(writer, event, (JFCGUIElement) event.getTarget());
}
}
else {
// a target was clicked repeatedly:
// the condensed sequence contains no mouseDowns or
// mouseUps anymore
// -> just generate another full click
generateFullClick(writer, event, (JFCGUIElement) event.getTarget());
}
}
private void handleMouseDoubleClick(BufferedWriter writer, Event event) throws IOException {
StructureNode multiClick = structure.add("MultipleMouseClick");
// first click
lastMouseClickEvent = multiClick.add("MouseClick");
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(),
DOUBLE_CLICK_DURATION, 501);
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(),
DOUBLE_CLICK_DURATION, 502);
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(),
DOUBLE_CLICK_DURATION, 500);
// second click
lastMouseClickEvent = multiClick.add("MouseClick");
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(),
DOUBLE_CLICK_DURATION, 501);
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(),
DOUBLE_CLICK_DURATION, 502);
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(),
DOUBLE_CLICK_DURATION, 500);
lastMouseClickEvent = null;
}
private void handleKeyboardFocusChange(BufferedWriter writer, Event event) throws IOException {
lastKeySequenceEvent = null;
writeFocusChangeEvent(writer, event);
}
private void handleMouseButtonDown(BufferedWriter writer, Event event) throws IOException {
commitFocusEvent();
lastKeySequenceEvent = null;
lastMouseClickEvent = new StructureNode("MouseClick");
lastMouseDownTarget = event.getTarget();
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(), EVENT_DURATION, 501);
}
private void handleMouseButtonUp(BufferedWriter writer, Event event) throws IOException {
lastKeySequenceEvent = null;
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(), EVENT_DURATION, 502);
}
private void handleMouseDragAndDrop(BufferedWriter writer, Event event) throws IOException {
commitFocusEvent();
MouseDragAndDrop dragEvent = (MouseDragAndDrop) event.getType();
lastMouseClickEvent = new StructureNode("MouseDrag");
lastMouseDownTarget = null;
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(), EVENT_DURATION,
dragEvent.getXStart(), dragEvent.getYStart(), 501);
writeMouseClickEvent(writer, event, (JFCGUIElement) event.getTarget(), EVENT_DURATION,
dragEvent.getX(), dragEvent.getY(), 506);
structure.children.add(lastMouseClickEvent);
}
private void handleKeyPressed(BufferedWriter writer, Event event) throws IOException {
commitFocusEvent();
if (lastKeySequenceEvent == null) {
lastKeySequenceEvent = structure.add("KeySequence");
}
lastKeyTypedEvent = lastKeySequenceEvent.add("KeyTyped");
writeKeyEvent(writer, event, 401);
}
private void handleKeyReleased(BufferedWriter writer, Event event) throws IOException {
commitFocusEvent();
writeKeyEvent(writer, event, 402);
}
private void handleTextInput(BufferedWriter writer, Event event) throws IOException {
List textEvents = ((TextInput) event.getType()).getTextInputEvents();
// just split the text event into its key events again
for (Event textEvent : textEvents) {
if (textEvent.getType() instanceof KeyPressed) {
handleKeyPressed(writer, textEvent);
}
else if (textEvent.getType() instanceof KeyReleased) {
handleKeyReleased(writer, textEvent);
}
}
}
private void getMenuElements(List elements, GUIModel model) {
for (IGUIElement child : elements) {
if (child instanceof JFCMenuButton || child instanceof JFCMenu) {
menuElements.put(((JFCGUIElement) child).getName().replaceAll("^\"|\"$", ""),
(JFCGUIElement) child);
}
getMenuElements(model.getChildren(child), model);
}
}
private Stack findMenuItemHierarchy(JFCGUIElement item) {
Stack elements = new Stack<>();
// find line that contains this menu item name
int lineOfItem = -1;
for (int i = 0; i < menuList.size(); i++) {
String name = "\"" + menuList.get(i).trim().toLowerCase() + "\"";
if (name.equals(item.getName().trim().toLowerCase())) {
lineOfItem = i;
}
}
// now go backwards until the toplevel menu is found
int oldIndent = Integer.MAX_VALUE;
for (int j = lineOfItem; j >= 0; j--) {
String stripped = menuList.get(j).replaceFirst("^ *", "");
int indent = menuList.get(j).length() - stripped.length();
if (indent < oldIndent) {
// this is a parent submenu
elements.push(menuElements.get(stripped));
oldIndent = indent;
}
}
return elements;
}
private void commitFocusEvent() {
if (lastFocusChangeEvent != null) {
structure.children.add(lastFocusChangeEvent);
lastFocusChangeEvent = null;
}
}
private void generateFullClick(BufferedWriter writer, Event event, JFCGUIElement target)
throws IOException
{
lastMouseClickEvent = new StructureNode("MouseClick");
lastMouseDownTarget = event.getTarget();
writeMouseClickEvent(writer, event, target, EVENT_DURATION, 501);
writeMouseClickEvent(writer, event, target, EVENT_DURATION, 502);
writeMouseClickEvent(writer, event, target, EVENT_DURATION, 500);
writeItemActionEvent(writer, event);
structure.children.add(lastMouseClickEvent);
structure.children.add(lastItemActionEvent);
lastMouseDownTarget = null;
lastMouseClickEvent = null;
}
private void writeJacaretoTail(BufferedWriter writer) throws IOException {
writeLine(writer, "");
// write the recording's structure
writeLine(writer, "");
writer.write(structure.toString());
// close root element
writeLine(writer, "");
}
/**
*
* Helper function that opens the replay file for writing.
*
*
* @param filename
* name and path of the replay file
* @param encoding
* file encoding, empty string for platform default
* @return {@link OutputStreamWriter} that writes to the replay file
*/
private OutputStreamWriter openReplayFile(String filename) {
File file = new File(filename);
boolean fileCreated;
try {
fileCreated = file.createNewFile();
if (!fileCreated) {
Console.traceln(Level.INFO, "Created logfile " + filename);
}
else {
Console.traceln(Level.INFO, "Overwrote existing logfile " + filename);
}
}
catch (IOException e) {
Console.printerrln("Unable to create file " + filename);
Console.logException(e);
}
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
}
catch (IOException e) {
Console.printerrln("Unable to open file for writing (read-only file):" + filename);
Console.logException(e);
}
return writer;
}
private void writeItemActionEvent(BufferedWriter writer, Event event) throws IOException {
JFCGUIElement target = (JFCGUIElement) event.getTarget();
MouseButtonInteraction info = (MouseButtonInteraction) event.getType();
// get rid of the quote symbols in the command because we want to
// escape the middle part for XML
String cmd = target.getName().substring(1, target.getName().length() - 1);
//@formatter:off
writeLine(writer,
""
);
writeLine(writer,
""
);
//@formatter:on
lastItemActionEvent = new StructureNode("ItemStateChange");
lastItemActionEvent.addRecordable();
lastItemActionEvent.addRecordable();
}
private void writeFocusChangeEvent(BufferedWriter writer, Event event) throws IOException {
KeyboardFocusChange info = (KeyboardFocusChange) event.getType();
JFCGUIElement target = (JFCGUIElement) event.getTarget();
if (currentFocus != null) {
lastFocusChangeEvent = new StructureNode("FocusChange");
// focus lost on old target
writeFocusEvent(writer, info, currentFocus, 1005);
// focus gained on new target
writeFocusEvent(writer, info, target, 1004);
}
else {
// the first focus event but that is not the case in autoquest, skip
}
currentFocus = target;
}
private void writeFocusEvent(BufferedWriter writer,
KeyboardFocusChange info,
JFCGUIElement target,
int jacId) throws IOException
{
//@formatter:off
writeLine(writer,
""
);
//@formatter:on
lastFocusChangeEvent.addRecordable();
}
private void writeMouseClickEvent(BufferedWriter writer,
Event event,
JFCGUIElement target,
int duration,
int jacId) throws IOException
{
MouseButtonInteraction info = (MouseButtonInteraction) event.getType();
writeMouseClickEvent(writer, event, target, duration, info.getX(), info.getY(), jacId);
}
private void writeMouseClickEvent(BufferedWriter writer,
Event event,
JFCGUIElement target,
int duration,
int x,
int y,
int jacId) throws IOException
{
MouseButtonInteraction info = (MouseButtonInteraction) event.getType();
int clickCount = event.getType() instanceof MouseDoubleClick ? 2 : 1;
//@formatter:off
writeLine(writer,
""
);
writeLine(writer,
""
);
writeLine(writer, "");
//@formatter:on
lastMouseClickEvent.addRecordable();
}
private int getButtonModifier(MouseButtonInteraction info) {
switch (info.getButton())
{
case LEFT:
return 16;
case MIDDLE:
return 8;
case RIGHT:
return 4;
default:
Console.traceln(Level.WARNING, "Unknown mouse button pressed.");
return -1;
}
}
private void writeKeyEvent(BufferedWriter writer, Event event, int jacId) throws IOException {
KeyInteraction info = (KeyInteraction) event.getType();
JFCGUIElement target = (JFCGUIElement) event.getTarget();
int keyCode = info.getKey().getVirtualKeyCode();
applyKeyModifier(info.getKey(), jacId == 401);
//@formatter:off
writeLine(writer,
""
);
writeLine(writer,
""
);
writeLine(writer, "");
lastKeyTypedEvent.addRecordable();
}
private String getKeyChar (int keyCode) {
if (keyCode >= 32 && keyCode < 127) {
return String.valueOf((char)keyCode);
}
return "_NO_LEGAL_XML_CHAR";
}
private void applyKeyModifier (VirtualKey key, boolean set) {
Integer modifier = modifiers.get(key);
if (modifier != null) {
if (set) {
currentKeyModifiers |= modifier;
}
else {
currentKeyModifiers &= ~modifier;
}
}
}
}