com.mirth.connect.server.builders.JavaScriptBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.mirth.connect.server.builders.JavaScriptBuilder.java

Source

/*
 * Copyright (c) Mirth Corporation. All rights reserved.
 * 
 * http://www.mirthcorp.com
 * 
 * The software in this package is published under the terms of the MPL license a copy of which has
 * been included with this distribution in the LICENSE.txt file.
 */

package com.mirth.connect.server.builders;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import com.mirth.connect.client.core.ControllerException;
import com.mirth.connect.model.CodeTemplate;
import com.mirth.connect.model.CodeTemplateLibrary;
import com.mirth.connect.model.ContextType;
import com.mirth.connect.model.Filter;
import com.mirth.connect.model.MetaData;
import com.mirth.connect.model.Rule;
import com.mirth.connect.model.Step;
import com.mirth.connect.model.Transformer;
import com.mirth.connect.model.util.JavaScriptConstants;
import com.mirth.connect.plugins.DataTypeServerPlugin;
import com.mirth.connect.server.controllers.CodeTemplateController;
import com.mirth.connect.server.controllers.ControllerFactory;
import com.mirth.connect.server.controllers.ExtensionController;
import com.mirth.connect.server.controllers.ScriptController;
import com.mirth.connect.util.CodeTemplateUtil;

public class JavaScriptBuilder {
    private static Logger logger = Logger.getLogger(JavaScriptBuilder.class);
    private static ExtensionController extensionController = ControllerFactory.getFactory()
            .createExtensionController();
    private static CodeTemplateController codeTemplateController = ControllerFactory.getFactory()
            .createCodeTemplateController();

    /*
     * Generates the global JavaScript contained in all new scopes created
     */
    public static String generateGlobalSealedScript() {
        StringBuilder script = new StringBuilder();

        // add #trim() function to JavaScript String prototype
        script.append(
                "String.prototype.trim = function() { return this.replace(/^\\s+|\\s+$/g,\"\").replace(/^\\t+|\\t+$/g,\"\"); };");

        for (MetaData metaData : extensionController.getConnectorMetaData().values()) {
            if (CollectionUtils.isNotEmpty(metaData.getUserutilPackages())) {
                for (String packageName : metaData.getUserutilPackages()) {
                    script.append("importPackage(").append(packageName).append(");\n");
                }
            }
        }

        for (MetaData metaData : extensionController.getPluginMetaData().values()) {
            if (CollectionUtils.isNotEmpty(metaData.getUserutilPackages())) {
                for (String packageName : metaData.getUserutilPackages()) {
                    script.append("importPackage(").append(packageName).append(");\n");
                }
            }
        }

        script.append("importPackage(Packages.com.mirth.connect.userutil);\n");
        script.append("importPackage(Packages.com.mirth.connect.server.userutil);\n");
        script.append("regex = new RegExp('');\n");
        script.append("xml = new XML('');\n");
        script.append("xmllist = new XMLList();\n");
        script.append("namespace = new Namespace();\n");
        script.append("qname = new QName();\n");

        /*
         * Ignore whitespace so blank lines are removed when deleting elements. This also involves
         * changing XmlProcessor.java in Rhino to account for Rhino issue 369394 and MIRTH-1405
         */
        script.append("XML.ignoreWhitespace=true;");
        // Setting prettyPrinting to true causes HL7 to break when converting back from HL7.
        script.append("XML.prettyPrinting=false;");

        return script.toString();
    }

    /*
     * Generation functions for general scripts
     */

    public static String generateScript(String channelId, String script, Set<String> scriptOptions,
            ContextType contextType) {
        StringBuilder builder = new StringBuilder();

        appendMapFunctions(builder);
        appendAttachmentFunctions(builder, scriptOptions);
        appendCodeTemplates(builder, channelId, contextType);
        appendMiscFunctions(builder);

        builder.append("function doScript() {\n" + script + " \n}\n");
        builder.append("doScript();\n");

        return builder.toString();
    }

    /*
     * Generation functions for specific phases
     */

    public static String generateDefaultKeyScript(String key) {
        return generateDefaultKeyScript(key, true);
    }

    public static String generateDefaultKeyScript(String key, boolean isGlobal) {
        String script = null;
        StringBuilder builder = new StringBuilder();

        if (key.equals(ScriptController.DEPLOY_SCRIPT_KEY)) {
            appendDefaultDeployScript(builder, isGlobal);
            script = builder.toString();
        } else if (key.equals(ScriptController.UNDEPLOY_SCRIPT_KEY)) {
            appendDefaultUndeployScript(builder, isGlobal);
            script = builder.toString();
        } else if (key.equals(ScriptController.PREPROCESSOR_SCRIPT_KEY)) {
            appendDefaultPreprocessorScript(builder, isGlobal);
            script = builder.toString();
        } else if (key.equals(ScriptController.POSTPROCESSOR_SCRIPT_KEY)) {
            appendDefaultPostprocessorScript(builder, isGlobal);
            script = builder.toString();
        }

        return script;
    }

    public static String generateFilterTransformerScript(Filter filter, Transformer transformer)
            throws BuilderException {
        logger.debug("generating script");

        StringBuilder builder = new StringBuilder();

        DataTypeServerPlugin inboundServerPlugin = ExtensionController.getInstance().getDataTypePlugins()
                .get(transformer.getInboundDataType());

        switch (inboundServerPlugin.getSerializationType()) {
        case JSON:
            builder.append("msg = JSON.parse(connectorMessage.getTransformedData());\n");
            break;

        case XML:
            // Turn the inbound message into an E4X XML object
            builder.append("msg = new XML(connectorMessage.getTransformedData());\n");

            // Set the default namespace if there is one left on the root node, otherwise set it to ''.
            builder.append(
                    "if (msg.namespace('') != undefined) { default xml namespace = msg.namespace(''); } else { default xml namespace = ''; }\n");
            break;

        case RAW:
            builder.append(
                    "if (connectorMessage.getProcessedRawData() != null) { msg = new String(connectorMessage.getProcessedRawData()); } else { msg = new String(connectorMessage.getRawData()); } \n");
            break;
        }

        // Turn the outbound template into an E4X XML object, if there is one
        if (StringUtils.isNotBlank(transformer.getOutboundTemplate())) {
            DataTypeServerPlugin outboundServerPlugin = ExtensionController.getInstance().getDataTypePlugins()
                    .get(transformer.getOutboundDataType());

            switch (outboundServerPlugin.getSerializationType()) {
            case JSON:
                builder.append("tmp = JSON.parse(template);\n");
                break;

            case XML:
                builder.append("tmp = new XML(template);\n");
                break;

            case RAW:
                builder.append("tmp = template;\n");
                break;
            }
        }

        // Append doFilter() function
        appendFilterScript(builder, filter);
        // Append doTransform() function
        appendTransformerScript(builder, transformer, false);
        // Append doFilter/doTransform execution
        builder.append("if (doFilter() == true) { doTransform(); return true; } else { return false; }");

        return builder.toString();
    }

    public static String generateResponseTransformerScript(Transformer transformer) throws BuilderException {
        logger.debug("Generating response transformer script...");

        StringBuilder builder = new StringBuilder();

        DataTypeServerPlugin inboundServerPlugin = ExtensionController.getInstance().getDataTypePlugins()
                .get(transformer.getInboundDataType());

        switch (inboundServerPlugin.getSerializationType()) {
        case JSON:
            // Turn the inbound message into a JavaScript object
            builder.append("msg = JSON.parse(connectorMessage.getResponseTransformedData());\n");
            break;

        case XML:
            // Turn the inbound message into an E4X XML object
            builder.append("msg = new XML(connectorMessage.getResponseTransformedData());\n");

            // Set the default namespace if there is one left on the root node, otherwise set it to ''.
            builder.append(
                    "if (msg.namespace('') != undefined) { default xml namespace = msg.namespace(''); } else { default xml namespace = ''; }\n");
            break;

        case RAW:
            builder.append("msg = new String(response.getMessage()) \n");
            break;
        }

        if (StringUtils.isNotBlank(transformer.getOutboundTemplate())) {
            DataTypeServerPlugin outboundServerPlugin = ExtensionController.getInstance().getDataTypePlugins()
                    .get(transformer.getOutboundDataType());

            switch (outboundServerPlugin.getSerializationType()) {
            case JSON:
                // Turn the outbound template into a JavaScript object, if there is one
                builder.append("tmp = JSON.parse(template);\n");
                break;

            case XML:
                // Turn the outbound template into an E4X XML object, if there is one
                builder.append("tmp = new XML(template);\n");
                break;

            case RAW:
                builder.append("tmp = template;\n");
                break;
            }
        }

        // Append doTransform() function
        appendTransformerScript(builder, transformer, true);
        // Append doTransform() execution
        builder.append("doTransform();");

        return builder.toString();
    }

    /*
     * General functions used by the generation methods to build scripts
     */

    private static void appendDefaultDeployScript(StringBuilder builder, boolean isGlobal) {
        if (isGlobal) {
            builder.append(JavaScriptConstants.DEFAULT_GLOBAL_DEPLOY_SCRIPT);
        } else {
            builder.append(JavaScriptConstants.DEFAULT_CHANNEL_DEPLOY_SCRIPT);
        }
    }

    private static void appendDefaultUndeployScript(StringBuilder builder, boolean isGlobal) {
        if (isGlobal) {
            builder.append(JavaScriptConstants.DEFAULT_GLOBAL_UNDEPLOY_SCRIPT);
        } else {
            builder.append(JavaScriptConstants.DEFAULT_CHANNEL_UNDEPLOY_SCRIPT);
        }
    }

    private static void appendDefaultPreprocessorScript(StringBuilder builder, boolean isGlobal) {
        if (isGlobal) {
            builder.append(JavaScriptConstants.DEFAULT_GLOBAL_PREPROCESSOR_SCRIPT);
        } else {
            builder.append(JavaScriptConstants.DEFAULT_CHANNEL_PREPROCESSOR_SCRIPT);
        }
    }

    private static void appendDefaultPostprocessorScript(StringBuilder builder, boolean isGlobal) {
        if (isGlobal) {
            builder.append(JavaScriptConstants.DEFAULT_GLOBAL_POSTPROCESSOR_SCRIPT);
        } else {
            builder.append(JavaScriptConstants.DEFAULT_CHANNEL_POSTPROCESSOR_SCRIPT);
        }
    }

    private static void appendFilterScript(StringBuilder builder, Filter filter) throws BuilderException {
        logger.debug("building javascript filter: rule count=" + filter.getRules().size());

        if (filter.getRules().isEmpty()) {
            logger.debug("filter is empty, setting to accept all messages");
            builder.append("function doFilter() { phase[0] = 'filter'; return true; }");
        } else {
            for (ListIterator<Rule> iter = filter.getRules().listIterator(); iter.hasNext();) {
                Rule rule = iter.next();

                if (rule.getType().equalsIgnoreCase("External Script")) {
                    try {
                        File externalScriptFile = new File(rule.getScript());
                        builder.append("function filterRule" + iter.nextIndex() + "() {\n"
                                + FileUtils.readFileToString(externalScriptFile) + "\n}");
                    } catch (IOException e) {
                        throw new BuilderException("Could not add script file.", e);
                    }
                } else {
                    builder.append("function filterRule" + iter.nextIndex() + "() {\n" + rule.getScript() + "\n}");
                }
            }

            builder.append("function doFilter() { phase[0] = 'filter'; return (");

            // call each of the above functions in a big boolean expression
            for (ListIterator<Rule> iter = filter.getRules().listIterator(); iter.hasNext();) {
                Rule rule = iter.next();
                String operator = "";

                if (rule.getOperator().equals(Rule.Operator.AND)) {
                    operator = " && ";
                } else if (rule.getOperator().equals(Rule.Operator.OR)) {
                    operator = " || ";
                }

                builder.append(operator + "(filterRule" + iter.nextIndex() + "() == true)");
            }

            builder.append("); }\n");
        }
    }

    private static void appendTransformerScript(StringBuilder builder, Transformer transformer, boolean response)
            throws BuilderException {
        logger.debug("building javascript transformer: step count=" + transformer.getSteps().size());

        // Set the phase and also reset the logger to transformer (it was filter before)
        builder.append("function doTransform() {");

        if (!response) {
            builder.append(
                    " phase[0] = 'transformer'; logger = Packages.org.apache.log4j.Logger.getLogger(phase[0]);");
        }

        builder.append("\n\n\n");

        for (Step step : transformer.getSteps()) {
            logger.debug("adding step: " + step.getName());

            if (step.getType().equalsIgnoreCase("External Script")) {
                try {
                    builder.append("\n" + FileUtils.readFileToString(new File(step.getScript())) + "\n");
                } catch (IOException e) {
                    throw new BuilderException("Could not add script file.", e);
                }
            } else {
                builder.append(step.getScript() + "\n");
            }
        }

        builder.append("if ('xml' === typeof msg) {\n");
        builder.append("    if (msg.hasSimpleContent()) {\n");
        builder.append("        msg = msg.toXMLString();\n");
        builder.append("    }\n");
        builder.append("} else if ('undefined' !== typeof msg && msg !== null) {\n");
        builder.append("    var toStringResult = Object.prototype.toString.call(msg);\n");
        builder.append("    if (toStringResult == '[object Object]' || toStringResult == '[object Array]') {\n");
        builder.append("        msg = JSON.stringify(msg);\n");
        builder.append("    }\n");
        builder.append("}\n");

        builder.append("if ('xml' === typeof tmp) {\n");
        builder.append("    if (tmp.hasSimpleContent()) {\n");
        builder.append("        tmp = tmp.toXMLString();\n");
        builder.append("    }\n");
        builder.append("} else if ('undefined' !== typeof tmp && tmp !== null) {\n");
        builder.append("    var toStringResult = Object.prototype.toString.call(tmp);\n");
        builder.append("    if (toStringResult == '[object Object]' || toStringResult == '[object Array]') {\n");
        builder.append("        tmp = JSON.stringify(tmp);\n");
        builder.append("    }\n");
        builder.append("}\n");

        builder.append("}\n");
    }

    private static void appendMiscFunctions(StringBuilder builder) {
        // Script used to check for existence of segment
        builder.append("function validate(mapping, defaultValue, replacement) {\n");
        builder.append("    var result = mapping;\n");
        builder.append("    if ((result == undefined) || (result.toString().length == 0)) {\n");
        builder.append("        if (defaultValue == undefined) {\n");
        builder.append("            defaultValue = '';\n");
        builder.append("        }\n");
        builder.append("        result = defaultValue;\n");
        builder.append("    }\n");
        builder.append(
                "    if ('string' === typeof result || result instanceof java.lang.String || 'xml' === typeof result) {\n");
        builder.append("        result = new java.lang.String(result.toString());\n");
        builder.append("        if (replacement != undefined) {\n");
        builder.append("            for (var i = 0; i < replacement.length; i++) { ");
        builder.append("                var entry = replacement[i];\n");
        builder.append("                result = result.replaceAll(entry[0], entry[1]);\n");
        builder.append("            }\n");
        builder.append("        }\n");
        builder.append("    }\n");
        builder.append("    return result;\n");
        builder.append("}\n");

        // Helper function to create segments
        builder.append("function createSegment(name, msgObj, index) {");
        builder.append("if (arguments.length == 1) { return new XML('<' + name + '></' + name + '>'); };");
        builder.append("if (arguments.length == 2) { index = 0; };");
        builder.append("msgObj[name][index] = new XML('<' + name + '></' + name + '>');");
        builder.append("return msgObj[name][index];");
        builder.append("}\n");

        // Helper function to create segments after specified field
        builder.append("function createSegmentAfter(name, segment) {");
        builder.append("var msgObj = segment;");
        builder.append("while (msgObj.parent() != undefined) { msgObj = msgObj.parent(); }");
        builder.append("msgObj.insertChildAfter(segment[0], new XML('<' + name + '></' + name + '>'));");
        builder.append("return msgObj.child(segment[0].childIndex() + 1);");
        builder.append("}\n");

        /*
         * Since we use a sealed shared scope everywhere, importClass won't be available. To allow
         * this to still work for migration, we override importClass to call importPackage instead.
         */
        builder.append("importClass = function() {\n");
        builder.append(
                "    logger.error('The importClass method has been deprecated and will soon be removed. Please use importPackage or the fully-qualified class name instead.');\n");
        builder.append("    for each (argument in arguments) {\n");
        builder.append("        var className = new Packages.java.lang.String(argument);\n");
        builder.append("        if (className.startsWith('class ')) {\n");
        builder.append("            className = className.substring(6);\n");
        builder.append("        }\n");
        builder.append(
                "        eval('importPackage(' + Packages.java.lang.Class.forName(className).getPackage().getName() + ')');\n");
        builder.append("    }\n");
        builder.append("}\n");

        // TODO: Replace the above with this in 3.1
        // builder.append("importClass = undefined;\n");
    }

    private static void appendMapFunctions(StringBuilder builder) {
        builder.append(
                "function $co(key, value) { if (arguments.length == 1) { return connectorMap.get(key); } else { return connectorMap.put(key, value); } }\n");
        builder.append(
                "function $c(key, value) { if (arguments.length == 1) { return channelMap.get(key); } else { return channelMap.put(key, value); } }\n");
        builder.append(
                "function $s(key, value) { if (arguments.length == 1) { return sourceMap.get(key); } else { return sourceMap.put(key, value); } }\n");
        builder.append(
                "function $gc(key, value) { if (arguments.length == 1) { return globalChannelMap.get(key); } else { return globalChannelMap.put(key, value); } }\n");
        builder.append(
                "function $g(key, value) { if (arguments.length == 1) { return globalMap.get(key); } else { return globalMap.put(key, value); } }\n");
        builder.append(
                "function $cfg(key, value) { if (arguments.length == 1) { return configurationMap.get(key); } else { return configurationMap.put(key, value); } }\n");
        builder.append(
                "function $r(key, value) { if (arguments.length == 1) { return responseMap.get(key); } else { return responseMap.put(key, value); } }\n");

        // No need to check the code context here; the function checks whether each individual map exists first 
        builder.append("function $(string) { ");
        builder.append("try { if(responseMap.containsKey(string)) { return $r(string); } } catch(e){}");
        builder.append("try { if(connectorMap.containsKey(string)) { return $co(string); } } catch(e){}");
        builder.append("try { if(channelMap.containsKey(string)) { return $c(string); } } catch(e){}");
        builder.append("try { if(sourceMap.containsKey(string)) { return $s(string); } } catch(e){}");
        builder.append("try { if(globalChannelMap.containsKey(string)) { return $gc(string); } } catch(e){}");
        builder.append("try { if(globalMap.containsKey(string)) { return $g(string); } } catch(e){}");
        builder.append("try { if(configurationMap.containsKey(string)) { return $cfg(string); } } catch(e){}");
        // TODO: This is temporary for the database reader and should not stay
        builder.append("try { if(resultMap.containsKey(string)) { return resultMap.get(string); } } catch(e){}");
        builder.append("return ''; }\n");
    }

    private static void appendAttachmentFunctions(StringBuilder builder, Set<String> scriptOptions) {
        // Helper function to access attachments (returns List<Attachment>)
        builder.append("function getAttachments() {");
        builder.append("return AttachmentUtil.getMessageAttachments(connectorMessage);");
        builder.append("}\n");

        // Helper function to set attachment
        if (scriptOptions != null && scriptOptions.contains("useAttachmentList")) {

            builder.append("function addAttachment(data, type) {\n");
            builder.append("return AttachmentUtil.addAttachment(mirth_attachments, data, type);\n");
            builder.append("}\n");
        } else {
            builder.append("function addAttachment(data, type) {\n");
            builder.append("return AttachmentUtil.createAttachment(connectorMessage, data, type);\n");
            builder.append("}\n");
        }
    }

    private static void appendCodeTemplates(StringBuilder builder, String channelId, ContextType contextType) {
        try {
            Map<String, CodeTemplate> codeTemplateMap = new HashMap<String, CodeTemplate>();
            for (CodeTemplate template : codeTemplateController.getCodeTemplates(null)) {
                codeTemplateMap.put(template.getId(), template);
            }

            for (CodeTemplateLibrary library : codeTemplateController.getLibraries(null, false)) {
                for (CodeTemplate template : library.getCodeTemplates()) {
                    // Ignore code templates that have already been handled
                    // Only add the code template if the library is enabled for this channel, or if it's not explicitly disabled and new channels are being included 
                    if (codeTemplateMap.containsKey(template.getId())
                            && (channelId == null || library.getEnabledChannelIds().contains(channelId)
                                    || (!library.getDisabledChannelIds().contains(channelId)
                                            && library.isIncludeNewChannels()))) {
                        CodeTemplate serverCodeTemplate = codeTemplateMap.get(template.getId());
                        if (serverCodeTemplate.isAddToScripts()
                                && serverCodeTemplate.getContextSet().contains(contextType)) {
                            builder.append(CodeTemplateUtil.stripDocumentation(serverCodeTemplate.getCode()));
                            builder.append('\n');
                        }
                    }

                    // This code template has been handled, remove it from the map
                    codeTemplateMap.remove(template.getId());
                }
            }
        } catch (ControllerException e) {
            logger.error("Could not append code templates.", e);
        }
    }
}