/* * 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; } } }