/*
* 09/16/2004
*
* Macro.java - A macro as recorded/played back by an RTextArea.
* Copyright (C) 2004 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/
package org.fife.ui.rtextarea;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
import org.xml.sax.InputSource;
import org.fife.io.UnicodeReader;
/**
* A macro as recorded/played back by an <code>RTextArea</code>.
*
* @author Robert Futrell
* @version 0.1
*/
public class Macro {
private String name;
private ArrayList macroRecords;
private static final String ROOT_ELEMENT = "macro";
private static final String MACRO_NAME = "macroName";
private static final String ACTION = "action";
private static final String ID = "id";
private static final String UNTITLED_MACRO_NAME = "<Untitled>";
private static final String FILE_ENCODING = "UTF-8";
/**
* Constructor.
*/
public Macro() {
this(UNTITLED_MACRO_NAME);
}
/**
* Loads a macro from a file on disk.
*
* @param file
* The file from which to load the macro.
* @throws java.io.EOFException
* If an EOF is reached unexpectedly (i.e., the file is corrupt).
* @throws FileNotFoundException
* If the specified file does not exist, is a directory instead of a regular file, or otherwise cannot
* be opened.
* @throws IOException
* If an I/O exception occurs while reading the file.
* @see #saveToFile
*/
public Macro(File file) throws EOFException, FileNotFoundException,
IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = null;
Document doc = null;
try {
db = dbf.newDocumentBuilder();
// InputSource is = new InputSource(new FileReader(file));
InputSource is = new InputSource(new UnicodeReader(
new FileInputStream(file), FILE_ENCODING));
is.setEncoding(FILE_ENCODING);
doc = db.parse(is);// db.parse(file);
} catch (Exception e) {
e.printStackTrace();
String desc = e.getMessage();
if (desc == null) {
desc = e.toString();
}
throw new IOException("Error parsing XML: " + desc);
}
macroRecords = new ArrayList();
// Traverse the XML tree.
boolean parsedOK = initializeFromXMLFile(doc.getDocumentElement());
if (parsedOK == false) {
name = null;
macroRecords.clear();
macroRecords = null;
throw new IOException("Error parsing XML!");
}
}
/**
* Constructor.
*
* @param name
* The name of the macro.
*/
public Macro(String name) {
this(name, null);
}
/**
* Constructor.
*
* @param name
* The name of the macro.
* @param records
* The initial records of the macro.
*/
public Macro(String name, List records) {
this.name = name;
if (records != null) {
macroRecords = new ArrayList(records.size());
Iterator i = records.iterator();
while (i.hasNext()) {
MacroRecord record = (MacroRecord) i.next();
macroRecords.add(record);
}
}
else {
macroRecords = new ArrayList(10);
}
}
/**
* Adds a macro record to this macro.
*
* @param record
* The record to add. If <code>null</code>, nothing happens.
* @see #getMacroRecords
*/
public void addMacroRecord(MacroRecord record) {
if (record != null)
macroRecords.add(record);
}
/**
* Returns the macro records that make up this macro.
*
* @return The macro records.
* @see #addMacroRecord
*/
public List getMacroRecords() {
return macroRecords;
}
/**
* Returns the name of this macro.
*
* @return The macro's name.
* @see #setName
*/
public String getName() {
return name;
}
/**
* Used in parsing an XML document containing a macro. This method initializes this macro with the data contained in
* the passed-in node.
*
* @param node
* The root node of the parsed XML document.
* @return <code>true</code> if the macro initialization went okay; <code>false</code> if an error occurred.
*/
private boolean initializeFromXMLFile(Element root) {
/*
* This method expects the XML document to be in the following format:
*
* <?xml version="1.0" encoding="UTF-8" ?> <macro> <macroName>test</macroName> <action
* id="default-typed">abcdefg</action> [<action id=...>...</action>] ... </macro>
*/
NodeList childNodes = root.getChildNodes();
int count = childNodes.getLength();
for (int i = 0; i < count; i++) {
Node node = childNodes.item(i);
int type = node.getNodeType();
switch (type) {
// Handle element nodes.
case Node.ELEMENT_NODE:
String nodeName = node.getNodeName();
if (nodeName.equals(MACRO_NAME)) {
NodeList childNodes2 = node.getChildNodes();
name = UNTITLED_MACRO_NAME;
if (childNodes2.getLength() > 0) {
node = childNodes2.item(0);
int type2 = node.getNodeType();
if (type2 != Node.CDATA_SECTION_NODE &&
type2 != Node.TEXT_NODE) {
return false;
}
name = node.getNodeValue().trim();
}
// System.err.println("Macro name==" + name);
}
else if (nodeName.equals(ACTION)) {
NamedNodeMap attributes = node.getAttributes();
if (attributes == null || attributes.getLength() != 1)
return false;
Node node2 = attributes.item(0);
MacroRecord macroRecord = new MacroRecord();
if (!node2.getNodeName().equals(ID)) {
return false;
}
macroRecord.id = node2.getNodeValue();
NodeList childNodes2 = node.getChildNodes();
int length = childNodes2.getLength();
if (length == 0) { // Could be empty "" command.
// System.err.println("... empty actionCommand");
macroRecord.actionCommand = "";
// System.err.println("... adding action: " + macroRecord);
macroRecords.add(macroRecord);
break;
}
else {
node = childNodes2.item(0);
int type2 = node.getNodeType();
if (type2 != Node.CDATA_SECTION_NODE &&
type2 != Node.TEXT_NODE) {
return false;
}
macroRecord.actionCommand = node.getNodeValue();
macroRecords.add(macroRecord);
}
}
break;
default:
break; // Skip whitespace nodes, etc.
}
}
// Everything went okay.
return true;
}
/**
* Saves this macro to a text file. This file can later be read in by the constructor taking a <code>File</code>
* parameter; this is the mechanism for saving macros.
*
* @param fileName
* The name of the file in which to save the macro.
* @throws IOException
* If an error occurs while generating the XML for the output file.
*/
public void saveToFile(String fileName) throws IOException {
/*
* This method writes the XML document in the following format:
*
* <?xml version="1.0" encoding="UTF-8" ?> <macro> <macroName>test</macroName> <action
* id="default-typed">abcdefg</action> [<action id=...>...</action>] ... </macro>
*/
try {
DocumentBuilder db = DocumentBuilderFactory.newInstance().
newDocumentBuilder();
DOMImplementation impl = db.getDOMImplementation();
Document doc = impl.createDocument(null, ROOT_ELEMENT, null);
Element rootElement = doc.getDocumentElement();
// Write the name of the macro.
Element nameElement = doc.createElement(MACRO_NAME);
rootElement.appendChild(nameElement);
// Write all actions (the meat) in the macro.
int numActions = macroRecords.size();
for (int i = 0; i < numActions; i++) {
MacroRecord record = (MacroRecord) macroRecords.get(i);
Element actionElement = doc.createElement(ACTION);
actionElement.setAttribute(ID, record.id);
if (record.actionCommand != null &&
record.actionCommand.length() > 0) {
// Remove illegal characters. I'm no XML expert, but
// I'm not sure what I'm doing wrong. If we don't
// strip out chars with Unicode value < 32, our
// generator will insert '<value>', which will cause
// our parser to barf when reading the macro back in
// (it says "Invalid XML character"). But why doesn't
// our generator tell us the character is invalid too?
String command = record.actionCommand;
for (int j = 0; j < command.length(); j++) {
if (command.charAt(j) < 32) {
command = command.substring(0, j);
if (j < command.length() - 1)
command += command.substring(j + 1);
}
}
Node n = doc.createCDATASection(command);
actionElement.appendChild(n);
}
rootElement.appendChild(actionElement);
}
// Dump the XML out to the file.
StreamResult result = new StreamResult(new File(fileName));
DOMSource source = new DOMSource(doc);
TransformerFactory transFac = TransformerFactory.newInstance();
Transformer transformer = transFac.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, FILE_ENCODING);
transformer.transform(source, result);
} catch (RuntimeException re) {
throw re; // Keep FindBugs happy.
} catch (Exception e) {
throw new IOException("Error generating XML!");
}
}
/**
* Sets the name of this macro.
*
* @param name
* The new name for the macro.
* @see #getName
*/
public void setName(String name) {
this.name = name;
}
/**
* A "record" of a macro is a single action in the macro (corresponding to a key type and some action in the editor,
* such as a letter inserted into the document, scrolling one page down, selecting the current line, etc.).
*/
static class MacroRecord {
public String id;
public String actionCommand;
public MacroRecord() {
this(null, null);
}
public MacroRecord(String id, String actionCommand) {
this.id = id;
this.actionCommand = actionCommand;
}
}
}