blob: 6ffb8fb58aff4fad60b004739ee254c6e54f0c8d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017 Fabio Zadrozny and others.
* 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:
* Fabio Zadrozny - initial API and implementation - http://eclip.se/8519
*******************************************************************************/
package org.eclipse.e4.core.macros.internal;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.eclipse.core.runtime.Assert;
import org.eclipse.e4.core.macros.IMacroInstruction;
import org.eclipse.e4.core.macros.IMacroInstructionFactory;
import org.eclipse.e4.core.macros.IMacroPlaybackContext;
import org.eclipse.e4.core.macros.MacroPlaybackException;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* A macro that is created from a sequence of instructions which are stored
* in-memory (and may be persisted later on).
*/
public class ComposableMacro implements IMacro {
public static final String XML_MACRO_TAG = "macro"; //$NON-NLS-1$
public static final String XML_INSTRUCTION_TAG = "instruction"; //$NON-NLS-1$
public static final String XML_ID_ATTRIBUTE = "id"; //$NON-NLS-1$
public static final String XML_BASE64_ATTRIBUTE = "base64"; //$NON-NLS-1$
public static final String XML_BASE64_ATTRIBUTE_TRUE = "true"; //$NON-NLS-1$
public static final String XML_DEFINITION_TAG = "definition"; //$NON-NLS-1$
public static final String XML_KEY_TAG = "key"; //$NON-NLS-1$
public static final String XML_VALUE_TAG = "value"; //$NON-NLS-1$
/**
* Provides the macro instruction id to an implementation which is able to
* recreate it.
*/
private final Map<String, IMacroInstructionFactory> fMacroInstructionIdToFactory;
/**
* The macro instructions which compose this macro.
*/
private List<IMacroInstruction> fMacroInstructions = new ArrayList<>();
private static class IndexAndPriority {
private final int fIndex;
private final int fPriority;
private IndexAndPriority(int index, int priority) {
fIndex = index;
fPriority = priority;
}
}
/**
* Map of an event to the current index of the macro instruction in
* fMacroInstructions and the priority for the given macro instruction.
*/
private final Map<Object, IndexAndPriority> fEventToPlacement = new HashMap<>();
/**
* @param macroInstructionIdToFactory
* Only macros instructions which have ids available as keys in the
* macroInstructionIdToFactory will be accepted.
*/
public ComposableMacro(Map<String, IMacroInstructionFactory> macroInstructionIdToFactory) {
fMacroInstructionIdToFactory = macroInstructionIdToFactory;
}
/**
* Checks whether the added macro instruction is suitable to be added to this
* macro.
*
* @param macroInstruction
* the macro instruction to be checked.
*/
private void checkMacroInstruction(IMacroInstruction macroInstruction) {
Assert.isTrue(
fMacroInstructionIdToFactory == null
|| fMacroInstructionIdToFactory.containsKey(macroInstruction.getId()),
String.format("Macro instruction: %s not properly registered through a %s extension point.", //$NON-NLS-1$
macroInstruction.getId(), MacroServiceImpl.MACRO_INSTRUCTION_FACTORY_EXTENSION_POINT));
}
/**
* Adds a new macro instruction to this macro.
*
* @param macroInstruction
* the macro instruction to be appended to this macro.
*/
public void addMacroInstruction(IMacroInstruction macroInstruction) {
checkMacroInstruction(macroInstruction);
fMacroInstructions.add(macroInstruction);
}
/**
* Adds a macro instruction to be added to the current macro being recorded.
* This method should be used when an event may trigger the creation of multiple
* macro instructions and only one of those should be recorded.
*
* @param macroInstruction
* the macro instruction to be added to the macro currently being
* recorded.
* @param event
* the event that triggered the creation of the macro instruction to
* be added. If there are multiple macro instructions added for the
* same event, only the one with the highest priority will be kept
* (if 2 events have the same priority, the last one will replace the
* previous one).
* @param priority
* the priority of the macro instruction being added (to be compared
* against the priority of other added macro instructions for the
* same event).
* @return true if the macro instruction was actually added and false otherwise.
*/
public boolean addMacroInstruction(IMacroInstruction macroInstruction, Object event, int priority) {
Assert.isNotNull(event);
IndexAndPriority currentIndexAndPriority = fEventToPlacement.get(event);
if (currentIndexAndPriority == null) {
addMacroInstruction(macroInstruction);
fEventToPlacement.put(event, new IndexAndPriority(fMacroInstructions.size() - 1, priority));
return true;
}
if (priority >= currentIndexAndPriority.fPriority) {
checkMacroInstruction(macroInstruction);
fMacroInstructions.set(currentIndexAndPriority.fIndex, macroInstruction);
fEventToPlacement.put(event, new IndexAndPriority(currentIndexAndPriority.fIndex, priority));
return true;
}
return false;
}
/**
* Clears information obtained during recording which should be no longer needed
* after the macro is properly composed.
*/
public void clearCachedInfo() {
fEventToPlacement.clear();
}
@Override
public void playback(IMacroPlaybackContext macroPlaybackContext) throws MacroPlaybackException {
for (IMacroInstruction macroInstruction : fMacroInstructions) {
macroInstruction.execute(macroPlaybackContext);
}
}
/**
* Actually returns the bytes to be written to the disk to be loaded back later
* on (the actual load and playback is later done by {@link SavedXMLMacro}).
*
* @return an UTF-8 encoded array of bytes which can be used to rerun the macro
* later on.
* @throws IOException
* if some error happens converting the macro to XML.
*/
public byte[] toXMLBytes() throws IOException {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://xml.org/sax/features/namespaces", false); //$NON-NLS-1$
factory.setFeature("http://xml.org/sax/features/validation", false); //$NON-NLS-1$
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); //$NON-NLS-1$
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); //$NON-NLS-1$
DocumentBuilder documentBuilder = factory.newDocumentBuilder();
Document document = documentBuilder.newDocument();
Element root = document.createElement(XML_MACRO_TAG);
for (IMacroInstruction macroInstruction : fMacroInstructions) {
Map<String, String> map = macroInstruction.toMap();
Iterator<Entry<String, String>> iterator = map.entrySet().iterator();
Element instructionElement = document.createElement(XML_INSTRUCTION_TAG);
instructionElement.setAttribute(XML_ID_ATTRIBUTE, macroInstruction.getId());
Element instructionDefinition = document.createElement(XML_DEFINITION_TAG);
while (iterator.hasNext()) {
Entry<String, String> entry = iterator.next();
instructionDefinition
.appendChild(setTextContent(document.createElement(XML_KEY_TAG), entry.getKey()));
instructionDefinition
.appendChild(setTextContent(document.createElement(XML_VALUE_TAG), entry.getValue()));
}
instructionElement.appendChild(instructionDefinition);
root.appendChild(instructionElement);
}
document.appendChild(root);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$
transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); //$NON-NLS-1$//$NON-NLS-2$
DOMSource source = new DOMSource(document);
StreamResult outputTarget = new StreamResult(outputStream);
transformer.transform(source, outputTarget);
return outputStream.toByteArray();
} catch (DOMException | IllegalArgumentException | ParserConfigurationException
| TransformerFactoryConfigurationError | TransformerException e) {
throw new IOException("Error converting macro to XML.", e); //$NON-NLS-1$
}
}
private Element setTextContent(Element element, String str) {
if (isAsciiPrintable(str)) {
element.setTextContent(str);
} else {
element.setTextContent(new String(Base64.getEncoder().encode(str.getBytes(StandardCharsets.UTF_8)),
StandardCharsets.UTF_8));
element.setAttribute(XML_BASE64_ATTRIBUTE, XML_BASE64_ATTRIBUTE_TRUE);
}
return element;
}
private static boolean isAsciiPrintable(String str) {
int length = str.length();
for (int i = 0; i < length; i++) {
char c = str.charAt(i);
if (!(c >= 32 && c < 127)) {
return false;
}
}
return true;
}
/**
* Provides the number of macro instructions which have been added to this
* macro.
*
* @return the number of macro instructions in this macro.
*/
public int getLength() {
return fMacroInstructions.size();
}
}