/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt ******************************************************************************/ package com.opendoorlogistics.core.scripts.io; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.HashMap; import java.util.UUID; import javax.xml.bind.Binder; import javax.xml.bind.DatatypeConverter; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.opendoorlogistics.api.components.ODLComponent; import com.opendoorlogistics.api.components.ODLComponentProvider; import com.opendoorlogistics.core.components.ODLGlobalComponents; import com.opendoorlogistics.core.scripts.ScriptConstants; import com.opendoorlogistics.core.scripts.elements.ComponentConfig; import com.opendoorlogistics.core.scripts.elements.InstructionConfig; import com.opendoorlogistics.core.scripts.elements.Option; import com.opendoorlogistics.core.scripts.elements.Script; import com.opendoorlogistics.core.scripts.elements.ScriptEditorType; import com.opendoorlogistics.core.scripts.utils.ScriptUtils; import com.opendoorlogistics.core.scripts.utils.ScriptUtils.OptionVisitor; import com.opendoorlogistics.core.utils.Serialization; import com.opendoorlogistics.core.utils.XMLUtils; import com.opendoorlogistics.core.utils.strings.Strings; final public class ScriptIO { /** * We maintain a singleton instances as there is considerable overhead in creating a JAXBContext * and according the java docs the Oracle implementation (and hopefully others) of JAXBContext * is thread-safe. See https://jaxb.java.net/faq/index.html#threadSafety */ private static final ScriptIO SINGLETON = new ScriptIO(); /** * Access the singleton. If we get problems in the future with using a singleton * we can just create new objects in the map * @return */ public static ScriptIO instance(){ return SINGLETON; } private final ODLComponentProvider components; private final JAXBContext context; private final JAXBContextByClass byClass = new JAXBContextByClass(); // private final Binder<Node> binder; private ScriptIO(ODLComponentProvider components) { this.components = components; try { context = JAXBContext.newInstance(Script.class); // binder = context.createBinder(); } catch (Throwable e) { throw new RuntimeException(e); } } private ScriptIO() { this(ODLGlobalComponents.getProvider()); } /** * Deep copy the script by serialising to xml and then deserialising. * UUID is preserved. * * @param script * @return */ public Script deepCopy(Script script) { Binder<Node> binder = context.createBinder(); Document xml = toXML(script,binder); Script ret = fromXML(xml,binder); ret.setUuid(script.getUuid()); return ret; } public final static String COMPONENT_CONFIG_NODE = "Config"; enum IOType { SERIALISE, JAXB, STRING } private static IOType getComponentConfigIOType(Class<? extends Serializable> cls) { if (String.class == cls) { return IOType.STRING; } else if (cls.isAnnotationPresent(XmlRootElement.class)) { return IOType.JAXB; } else if (Serializable.class.isAssignableFrom(cls)) { return IOType.SERIALISE; } else { throw new RuntimeException("Could not save component to XML. It is not a string, a JAXB object or Serializable"); } } public Script fromFile(File file) { Document doc = XMLUtils.load(file); // generate uuid from filename Binder<Node> binder =context.createBinder(); Script script = fromXML(doc,binder); script.setUuid(getScriptUUID(file)); if (script.getScriptEditorUIType() == null) { script.setScriptEditorUIType(ScriptEditorType.WIZARD_GENERATED_EDITOR); } return script; } public static UUID getScriptUUID(File file) { return UUID.nameUUIDFromBytes(file.getAbsolutePath().getBytes()); } public static UUID getGlobalInstructionUUID(UUID scriptUuid, InstructionConfig instruction) { StringBuilder builder = new StringBuilder(scriptUuid.toString()); builder.append("-"); builder.append(instruction.getUuid()); UUID ret = UUID.nameUUIDFromBytes(builder.toString().getBytes()); return ret; } private Script fromXML(Node node,Binder<Node> binder ) { // sometimes node is document root and we need to go to child or grandchild boolean found = false; while (found == false && node != null) { found = Strings.equalsStd(node.getNodeName(), ScriptConstants.SCRIPT_XML_NODE_NAME); if (!found) { node = node.getFirstChild(); } } if (!found) { throw new RuntimeException("Cannot find node " + ScriptConstants.SCRIPT_XML_NODE_NAME + ". Is this a script file?"); } // update old versions of the script... updateOldVersions(node); try { Script script = (Script) binder.unmarshal(node); ScriptUtils.visitOptions(script, new OptionVisitor() { @Override public boolean visitOption(Option parent, Option option, int depth) { for (ComponentConfig conf : option.getComponentConfigs()) { unmarshalComponentConfig(conf); } for (ComponentConfig instruction : option.getInstructions()) { unmarshalComponentConfig(instruction); } return true; } private void unmarshalComponentConfig(ComponentConfig instruction) { Element instructionNode = (Element) binder.getXMLNode(instruction); NodeList nodeList = instructionNode.getElementsByTagName(COMPONENT_CONFIG_NODE); if (nodeList.getLength() > 0) { Node configNode = nodeList.item(0); // get the component ODLComponent component = components.getComponent(instruction.getComponent()); if (component == null) { throw new RuntimeException("Unknown component \"" + instruction.getComponent() + "\""); } // read the class type and deserialise according to this try { Class<? extends Serializable> cls = component.getConfigClass(); if (cls != null) { switch (getComponentConfigIOType(cls)) { case JAXB: Node componentNode = configNode.getFirstChild(); if (componentNode != null) { JAXBContext compContext = byClass.get(cls); instruction.setComponentConfig((Serializable) compContext.createUnmarshaller().unmarshal(componentNode)); } break; case STRING: instruction.setComponentConfig(configNode.getTextContent()); break; case SERIALISE: byte[] bytes = DatatypeConverter.parseBase64Binary(configNode.getTextContent()); instruction.setComponentConfig(Serialization.convertFromBytes(bytes, cls.getClassLoader())); break; } } } catch (Exception e) { throw new RuntimeException(e); } } } }); // ensure ids are unique ScriptUtils.validateIds(script); return script; } catch (Throwable e) { throw new RuntimeException(e); } } private void updateOldVersions(Node node) { // correct mis-spelling of synchronised if (Element.class.isInstance(node)) { Element element = (Element) node; if (element.hasAttribute("sychronised")) { String value = element.getAttribute("sychronised"); element.removeAttribute("sychronised"); element.setAttribute("synchronised", value); } } } public String toXMLString(Script script) { Binder<Node> binder =context.createBinder(); Document document = toXML(script,binder); if (document != null) { return XMLUtils.toString(document, XMLUtils.getPrettyPrintFormat()); } return null; } private Document toXML(Script script, Binder<Node> binder ) { try { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); // root elements final Document doc = docBuilder.newDocument(); binder.marshal(script, doc); ScriptUtils.visitOptions(script, new OptionVisitor() { @Override public boolean visitOption(Option parent, Option option, int depth) { try { for (ComponentConfig config : option.getComponentConfigs()) { marshallComponentConfig(doc, binder,config); } for (ComponentConfig config : option.getInstructions()) { marshallComponentConfig(doc,binder, config); } } catch (Exception e) { throw new RuntimeException(e); } for (ComponentConfig config : option.getInstructions()) { try { marshallComponentConfig(doc,binder, config); } catch (Exception e) { throw new RuntimeException(e); } } return true; } }); return doc; } catch (Throwable e) { e.printStackTrace(); throw new RuntimeException(e); } } /** * @param doc * @param config * @throws JAXBException * @throws IOException */ private void marshallComponentConfig(Document doc,Binder<Node> binder, ComponentConfig config) throws JAXBException, IOException { Node instructionNode = binder.getXMLNode(config); Serializable componentConf = config.getComponentConfig(); if (componentConf != null) { Node confNode = doc.createElement(COMPONENT_CONFIG_NODE); instructionNode.appendChild(confNode); switch (getComponentConfigIOType(componentConf.getClass())) { case JAXB: // marshall using JAXB... JAXBContext compContext = byClass.get(componentConf.getClass()); Marshaller m = compContext.createMarshaller(); m.marshal(componentConf, confNode); break; case SERIALISE: byte[] bytes = Serialization.convertToBytes((Serializable) componentConf); String encoded = DatatypeConverter.printBase64Binary(bytes); confNode.setTextContent(encoded); break; case STRING: confNode.setTextContent((String) componentConf); break; } } } private static class JAXBContextByClass{ private HashMap<Class<? extends Serializable>, JAXBContext>map = new HashMap<>(); synchronized JAXBContext get(Class<? extends Serializable> cls){ JAXBContext ret = map.get(cls); if(ret==null){ try{ ret = JAXBContext.newInstance(cls); }catch(Exception e){ throw new RuntimeException(e); } map.put(cls, ret); } return ret; } } }