diff --git a/src/MirthMigrator.java b/src/MirthMigrator.java deleted file mode 100644 index 483fa5b..0000000 --- a/src/MirthMigrator.java +++ /dev/null @@ -1,8683 +0,0 @@ -package lu.hrs.mirth.migration; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.ProtocolException; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.security.KeyManagementException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; -import javax.net.ssl.TrustManager; - -import org.apache.commons.io.IOUtils; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.XML; -import org.mozilla.javascript.Context; -import org.mozilla.javascript.ContextFactory; -import org.mozilla.javascript.NativeArray; -import org.mozilla.javascript.NativeJSON; -import org.mozilla.javascript.NativeObject; -import org.mozilla.javascript.Scriptable; -import org.mozilla.javascript.json.JsonParser; -import org.mozilla.javascript.json.JsonParser.ParseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Provides the API that allows the Mirth Migrator to communicate w/ the configured Mirth instances - * - * License: MPL 2.0 - * Project home: https://github.com/odoodo/Mirth-Migrator - * - * @author ortwin.donak - * - */ -public class MirthMigrator { - - private final static String version = "1.0.2"; - - private static String defaultServerName = "localhost"; - private static int defaultServerPort = 8443; - - /** The identifier or the component type channel */ - public final static String CHANNEL = "channel"; - /** The identifier or the component type code template */ - public final static String CODE_TEMPLATE = "codeTemplate"; - /** The identifier or the component type channel group */ - public final static String CHANNEL_GROUP = "channelGroup"; - /** The identifier or the component type channel tags */ - public final static String CHANNEL_TAG = "channelTag"; - /** The identifier or the component type inter-channel dependencies */ - public final static String INTER_CHANNEL_DEPENDENCY = "interChannelDependency"; - /** The identifier or the component type channel pruning */ - public final static String CHANNEL_PRUNING = "channelPruning"; - /** The identifier or the component type code template library */ - public final static String CODE_TEMPLATE_LIBRARY = "codeTemplateLibrary"; - /** - * The component type could not be determined (usually because the component definition did not contain any indicator that would allow to - * determine the type) - */ - public final static String UNKNOWN = "unknown"; - /** Key for decrypting user credentials */ - private final static String CREDENTIALS_KEY = "}G~8.I$+dC4ObH2qG\\VM4088<115Hyf]W=7Nf`6bi@%'^4_uO4"; - - private final static DateFormat displayDate = new SimpleDateFormat("dd.MM.yyyy, HH:mm:ss"); - private final static String clientIdentifier = "MirthMigrator"; - - /** used for parsing the change date in the channel description */ - private final static SimpleDateFormat changeParseDateFormat = new SimpleDateFormat("yyyyMMdd"); - /** used for formatting the change date in the channel details table */ - private final static SimpleDateFormat changeDisplayDateFormat = new SimpleDateFormat("dd.MM.yyyy"); - - private MirthVersion mirthVersion = null; - - private String systemName, environment, description, server, username, password, hash; - private int port; - - // indicates when the client was last updated (this is needed for automated refresh) - private Long lastUpdate = null; - - /** - * A cash for the Mirth client instances used to access the different Mirth systems. Those are shared by all sessions - */ - private static HashMap mirthClients = null; - - /** - * The location at which the Mirth Migrator configuration file can be found - */ - private final static String configurationFileLocation = ".\\web\\MirthMigrator\\config\\MirthMigration.conf"; - - /** - * The configured Mirth environments - */ - private static HashMap> mirthEnvironments = null; - - /** - * The Mirth Migrator configuration - */ - private static JSONObject configuration = null; - - /** - * A list of functions that should not be recognized as custom functions - */ - private static ArrayList functionFilter = new ArrayList(); - - /** - * The session cookie of the current session. - */ - private String serverSessionCookie; - - /** - * Used to extract passwords from the configuration file - */ - private final static Pattern passwordPattern = Pattern.compile("(\"password\"\\s*:\\s*\")([^\"]*)(\")"); - - /** - * Provides the function names and parameter list in code templates - */ - private final static Pattern credentialSplitPattern = Pattern.compile("^([^\\:]+)\\:(.+)"); - - /** - * Provides the function names and parameter list in code templates - */ - private final static Pattern functionNamePattern = Pattern.compile("\\s*function\\s+(\\w+)\\s*(\\(.*\\))\\s*\\{", Pattern.MULTILINE); - - /** - * Detects change log in channel descriptions with the following format: : - */ - private final static Pattern changesPattern = Pattern.compile("(\\d{8})\\:? *\\s*(.+)[\\s ]*"); - - /** - * Detects configurations of external system interfaces that are used for the visualization of the communication between IT systems via Mirth - * (This is another in-house tool guys, you can ignore it ;-) ) - */ - private final static Pattern systemInterfacePattern = Pattern.compile("(IN|OUT)\\:(\\d+|\\*)\\:([^\\:]+)\\:([^\\:\\r\\n]+)(?::([^\\:\\r\\n]+))?"); - - /** - * Provides function names of functions that are used within code - */ - private final static Pattern functionReferenceDetectionPattern = Pattern.compile("[^\\w\\.\\\\](\\w+)(?=\\()"); - /** - * Very rough detection of regular expressions. It should be exact enough for filtering false positives at function detection - */ - private final static Pattern roughRegexDetectionPattern = Pattern.compile("/[^/\\r\\n]+/"); - /** - * Detects all comments in code - */ - private final static Pattern base64DetectionPattern = Pattern.compile("(encoding=\"base64\"\\>)[^<]+"); - /** - * Detects all comments in code - */ - private final static Pattern commentDetectionPattern = Pattern.compile("(?ms)/(?:/.*?$|\\*.*?\\*/)"); - /** - * Detects all javascript strings (string in apostrophes) in code - */ - private final static Pattern javascriptStringDetectionPattern = Pattern.compile("\\'.*?\\'"); - /** - * Detects all java strings (quoted strings) in code - */ - private final static Pattern javaStringDetectionPattern = Pattern.compile("\\".*?\\""); - /** - * Detects all instantiations of new objects (like "new String()") - */ - private final static Pattern instantationDetectionPattern = Pattern.compile("\\bnew\\s+([a-zA-Z10-9]+)\\s*\\("); - /** - * Detects all regular expressions - */ - private final static Pattern regexDetectionPattern = Pattern.compile( - "\\/((?![*+?])(?:[^\\r\\n\\[/\\\\]|\\\\.|\\[(?:[^\\r\\n\\]\\\\]|\\\\.)*\\])+)\\/((?:g(?:im?|mi?)?|i(?:gm?|mg?)?|m(?:gi?|ig?)?)?)"); - /** - * Detects the id of a component - */ - private final static Pattern idPattern = Pattern.compile("([^<]+)<\\/id>"); - /** - * Detects all descriptions - */ - private final static Pattern descriptionTagDetectionPattern = Pattern.compile(".*?", Pattern.DOTALL); - /** - * Detects all subject tags - */ - private final static Pattern subjectTagDetectionPattern = Pattern.compile(".*?", Pattern.DOTALL); - /** - * Detects all name tags - */ - private final static Pattern nameTagDetectionPattern = Pattern.compile(".*?", Pattern.DOTALL); - /** - * Detects all empty tags (often used for encapsulating SQL) - */ - private final static Pattern emptyTagDetectionPattern = Pattern.compile("\\<\\>.*?\\</\\>", Pattern.DOTALL); - /** - * Detects all SQL queries w/i CDATA tags - */ - private final static Pattern cdataDetectionPattern = Pattern.compile("\\<\\!\\[CDATA\\[[\\s\\S]*?\\]\\]\\>", Pattern.DOTALL); - - /** - * Detects all SQL queries w/i query tags - */ - private final static Pattern queryDetectionPattern = Pattern.compile("\\<query\\>.*?\\<\\/query\\>", Pattern.DOTALL); - - /** - * Detects all descriptions - */ - private final static Pattern selectTagDetectionPattern = Pattern.compile("", Pattern.DOTALL); - /** - * This pattern is used to extract the JavaScript Doc header (group 1) as well as the name (group 2) from a function - */ - - private final static Pattern functionHeaderPattern = Pattern - .compile("(?:\\/\\*\\*((?:[\\s\\S](?!\\*\\/))*.)\\*\\/\\s*){0,1}function\\s*([^\\s\\(]+)\\s*\\(([^\\)]*)\\)", Pattern.DOTALL); - - /** - * This pattern is used to extract the JavaScript Doc header (group 1) from a code template that does not contain functions - */ - private final static Pattern codeTemplateHeaderPattern = Pattern.compile("\\/\\*\\*((?:[\\s\\S](?!\\*\\/))*.)\\*\\/", Pattern.DOTALL); - /** - * Provides the function description from the JavaScript Doc header - */ - private final static Pattern codeTemplateHeaderDescriptionPattern = Pattern.compile("^[\\p{Cntrl}\\s]+([^@]*)"); - /** - * Provides the Parameter descriptions from the JavaScript Doc header - */ - private final static Pattern codeTemplateHeaderParameterPattern = Pattern.compile("@param\\s+(?:\\{.*?\\})?\\s*?([^@\\s]+)([^@]+)"); - /** - * Extracts the parameters from a function definition - */ - private final static Pattern functionParameterPattern = Pattern.compile("[^\\s ,]+"); - - /** - * Provides the return value description from the JavaScript Doc header - * - */ - private final static Pattern codeTemplateHeaderReturnValuePattern = Pattern.compile("@return\\s+(?:\\{.*?\\})?\\s*?([^@]+)"); - // private final static Pattern CodeTemplateDescriptionPattern = Pattern.compile("\\/\\*\\*([^@\\*]*)"); - /** - * This pattern is used to find all occurrences of a components version. - */ - private final static Pattern mirthVersionConversionPattern = Pattern.compile("version=\\\"[^\\\"]*\\\""); - - /** - * This pattern is used to find a status code - */ - private final static Pattern statusPattern = Pattern.compile("([^<]+)<\\/status>"); - - /** - * This pattern is used to find an id encapsulated in a string tag. This is usually a channel id. - */ - private final static Pattern stringPattern = Pattern.compile("([^<]+)<\\/string>"); - - /** - * This pattern is used to find a status message - */ - private final static Pattern messagePattern = Pattern.compile("([^<]+)<\\/message>"); - - /** - * This pattern is used to find a channel tag - */ - private final static Pattern channelTagPattern = Pattern.compile(""); - - /** - * This pattern is used to find a inter-channel dependency - */ - private final static Pattern interChannelDependencyPattern = Pattern.compile(""); - - /** - * This pattern is used to find inter-channel dependency details: group 1 indicates the channel that depends on another channel; group 2 indicates the channel that must be started first - */ - private final static Pattern interChannelDependencyDetailsPattern = Pattern.compile("([^<]+)[\\s]*([^<]+)"); - - /** - * This pattern is used to find a channel pruning configuration - */ - private final static Pattern pruningPattern = Pattern.compile(""); - - /** - * This pattern is used to find a channel pruning configuration - */ - private final static Pattern externalResourcesPattern = Pattern.compile(""); - - /** - * This pattern is used to find a channel pruning configuration - */ - private final static Pattern externalResourceEntityPattern = Pattern.compile("[\\s\\S]*?([^<]*)[\\s\\S]*?([^<]*)[\\s\\S]*?"); - - /** - * This pattern is used to find channel references - */ - private final static Pattern channelIdPattern = Pattern.compile(""); - - /** - * This pattern is used to find channel definitions - */ - private final static Pattern channelPattern = Pattern.compile(""); - - /** - * This pattern is used to find channel references in code template libraries - */ - private final static Pattern channelReferencesPattern = Pattern.compile(""); - - /** - * This pattern is used to find code template definitions - */ - private final static Pattern codeTemplatePattern = Pattern.compile(""); - - /** - * This pattern is used to find code template references in code template libraries - */ - private final static Pattern codeTemplateReferencesPattern = Pattern.compile(""); - - // get channel group - private final static Pattern channelGroupPattern = Pattern.compile(""); - // get code template library - private final static Pattern codeTemplateLibraryPattern = Pattern - .compile("(?:(?!codeTemplateLibrary)[\\s\\S])*"); - // get channel group id (group 1): - // private final static Pattern groupIdPattern = Pattern.compile("([\\s\\S]*?)<\\/id>"); - // get channel group name (group 1) - private final static Pattern namePattern = Pattern.compile("([\\s\\S]*?)<\\/name>"); - // get the channels section of the channel group - private final static Pattern channelGroupChannelsPattern = Pattern.compile("([\\s\\S]*?)<\\/channels>"); - // get the codeTemplates section of the code template library - private final static Pattern codeTemplateLibraryCodeTemplatesPattern = Pattern.compile("([\\s\\S]*?)<\\/codeTemplates>"); - // a pattern used to separate the original id (of the source system) from the new id (for the destination system because of a detected id collision) - private final static Pattern idSeparatorPattern = Pattern.compile("([^\\:]+):(.+)"); - - private static Logger logger = null; - private static JsonParser jsonParser = null; - - // maps code template meta information to the code template id - private HashMap codeTemplateInfo = null; - // maps code template name to code template id - private HashMap codeTemplateIdbyName = null; - // maps code template id to code template name - private HashMap codeTemplateNameById = null; - // maps code template Id to the names of the functions that reside in this code template - private HashMap> codeTemplateIdToFunction = null; - // maps channel meta information to the channel id - private HashMap channelInfo = null; - // maps a channel to a list of libraries that are referenced by it - private HashMap> channelCodeTemplateLibraryReferences = null; - // Information about the state of all channels (activated or deactivated) - private HashMap channelState = null; - // Information about the last modified date in milliseconds for every channel identified by it's id - private HashMap channelLastModified = null; - // a list of functions used by a channel and properly linked - private HashMap> channelFunctionReferences = null; - // a list of functions that are defined within the channel itself - private HashMap> channelInternalFunctionsByChannelId = null; - // a list of functions that are referenced by a channel but of which the definition could not be identified (in case of false positives these have to be added to the filter list) - private HashMap> unknownChannelFunctions = null; - // a list of functions that are referenced by a function but of which the definition could not be identified (in case of false positives these have to be added to the filter list) - private HashMap> unknownFunctionFunctions = null; - // Resolves a channel name to it's id - private HashMap channelIdbyName = null; - // Resolves a channel id to it's name - private HashMap channelNameById = null; - // a list of channels that are actually using a function - private HashMap> channelReferencesToFunction = null; - // a list of functions that are actually using a function - private HashMap> functionLinkedByFunctions = null; - // a list of functions that are used by a function - private HashMap> functionUsesFunctions = null; - // A link between functions and the code template to which they belong - private HashMap codeTemplateIdByFunctionName = null; - // maps channel group meta information to the channel group id - private HashMap channelGroupInfo = null; - // provides channel groups in alphabetical order - private TreeMap channelGroupOrder = null; - // maps code template library meta information to the code template library id - private HashMap codeTemplateLibraryInfo = null; - // maps a code template id to a code template library id - private HashMap codeTemplateLibraryIdByCodeTemplateId = null; - // provides code template libraries in alphabetical order - private TreeMap codeTemplateLibraryOrder = null; - // provides information about all external resources that are referenced by the mirth instance - private HashMap externalResources = null; - // stores inter-channel dependencies - ToDo: still has to be implemented - private HashMap interChannelDependencies = null; - // stores detected code template conflicts - private HashMap> functionConflicts = null; - - // stores user sessions - private static final Map> userSessionCache = Collections.synchronizedMap(new HashMap>()); - - /** Determines the maximum inactivity period of a user session before it will automatically be ended */ - private static Integer userSessionLifeSpanInMinutes = 20; - - /** The point of time at which the configuration has last been loaded */ - private static Long configurationLoadingDate = null; - - static { - - MirthMigrator.logger = LoggerFactory.getLogger(MirthMigrator.class.getName()); - /* usually log level should be set via log4j properties file - - For log42 properties this would be: - =================================== - log4j.logger.lu.hrs.mirth.migration.MirthMigrator = DEBUG - - For log4j2 properties this would be: - ==================================== - logger.MirthMigrator.name = lu.hrs.mirth.migration.MirthMigrator - logger.MirthMigrator.level = DEBUG - */ -// MirthMigrator.logger.info("Mirth Migrator version " + getVersion() + " activated"); - - - - // instantiate a JsonParser for transferring JSON-structures to JavaScript - Context context = (new ContextFactory()).enterContext(); - Scriptable scriptable = context.initStandardObjects(); - MirthMigrator.jsonParser = new JsonParser(context, scriptable); - } - - public static String getVersion() { - return MirthMigrator.version; - } - - /** - * Creates a Mirth client instance - * - * @param systemName - * The name of this Mirth instance - * @param environment - * The Mirth environment to which this Mirth instance belongs - * @param server - * The name of the server at which the Mirth service is located - * @param port - * The port under which the server will be accessible - * @param user - * The predefined user name to log in with - * @param password - * The predefined password to log in with - * @param description - * A description of the Mirth system - * @throws ServiceUnavailableException - */ - private MirthMigrator(String systemName, String environment, String server, int port, String user, String password, String description) - throws ServiceUnavailableException { - - setSystemName(systemName); - setEnvironment(environment); - setServer(server); - setPort(port); - setUsername(user); - setPassword(password); - setDescription(description); - - // disable certificate validation - trustAll(); - - // create a hash value for the relevant server parameters (is intented to be used for smart config reload whenever I get some time) - setHash(createHash(String.format("%s_%d_%s_%s", server, port, user, password))); - } - - /** - * This function overrides the TrustManager, so that the self-signed certificate of Mirth is not validated. - */ - private static void trustAll() { - SSLContext sc; - try { - sc = SSLContext.getInstance("TLS"); - sc.init(null, new TrustManager[] { new TrustAllX509TrustManager() }, new java.security.SecureRandom()); - HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); - HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { - public boolean verify(String string, SSLSession ssls) { - return true; - } - }); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } catch (KeyManagementException e) { - e.printStackTrace(); - } - } - - /** - * Allows to set a custom API port that is used for user login - * - * @param apiPort - * The API port - */ - public static void setApiPort(int apiPort) { - if (apiPort > 0) { - // change the port that is used for user login - defaultServerPort = apiPort; - logger.debug("changed port for user login to " + defaultServerPort); - } - } - - /** - * Allows to set a custom API host that is used for user login - * - * @param apiHost - * The API host - */ - public static void setApiHost(String apiHost) { - if ((apiHost != null) && (apiHost.trim().length() > 0)) { - // change the host that is used for user login - defaultServerName = apiHost.trim(); - logger.debug("changed port for user login to " + defaultServerName); - } - } - - /** - * Validates a provided user account against a Mirth instance and creates a user session if the validation process was successful - * - * @param credentials - * The encrypted user credentials - * @return The session id or null if session could not be created - * @throws ServiceUnavailableException - */ - public static String createUserSession(String credentials) throws ServiceUnavailableException { - - // split the credentials - Matcher credentialSplitMatcher = credentialSplitPattern.matcher(decrypt(credentials)); - if (credentialSplitMatcher.find()) { - // get username - String username = credentialSplitMatcher.group(1); - // and password - String password = credentialSplitMatcher.group(2); - - // create the user session - return createUserSession(username, password, null, null); - } else { - return null; - } - } - - /** - * Encrypts a string - * @param text The text that should be encrypted - * @return The encrypted string - */ - public static String encrypt(String text) { - String key = MirthMigrator.CREDENTIALS_KEY; - StringBuilder encrypted = new StringBuilder(); - - for (int i = 0; i < text.length(); i++) { - // encrypt by XORing each character - int charCode = (text.charAt(i) ^ key.charAt(i % key.length())) % 255; - // add the encrypted char to the final result by assuring 2 characters are used per character - encrypted.append(String.format("%02x", charCode)); - } - - return encrypted.toString(); - } - - /** - * Decrypts a string - * - * @param encryptedText - * The encrypted string - * @return The decrypted string - */ - private static String decrypt(String encryptedText) { - String key = MirthMigrator.CREDENTIALS_KEY; - - StringBuilder decrypted = new StringBuilder(); - // extract hex values - Pattern pattern = Pattern.compile(".{1,2}"); - Matcher matcher = pattern.matcher(encryptedText); - - int index = 0; - // convert all values - while (matcher.find()) { - // get next hex value - String hexChunk = matcher.group(); - // decode it (simple XOR "encryption") - int charCode = Integer.parseInt(hexChunk, 16) ^ key.charAt(index++ % key.length()) % 255; - // and add it to the decoded string - decrypted.append((char) charCode); - } - - // return the decrypted string - return decrypted.toString(); - } - - /** - * Validates a provided user account against a Mirth instance and creates a user session if the validation process was successful - * - * @param username - * The name of the user for whom the session is requested. - * @param password - * The password of the user for whom the session is requested - * @return The session id or null if session could not be created - * @throws ServiceUnavailableException - */ - public static String createUserSession(String username, String password) throws ServiceUnavailableException { - return createUserSession(username, password, null, null); - } - - /** - * Validates a provided user account against a Mirth instance and creates a user session if the validation process was successful - * - * @param username - * The name of the user for whom the session is requested. - * @param password - * The password of the user for whom the session is requested - * @param serverPort - * A port at which the Mirth service is listening
- * (OPTIONAL - default: 8443) - * @return The session id or null if session could not be created - * @throws ServiceUnavailableException - */ - public static String createUserSession(String username, String password, Integer serverPort) throws ServiceUnavailableException { - return createUserSession(username, password, serverPort, null); - } - - /** - * Validates a provided user account against a Mirth instance and creates a user session if the validation process was successful - * - * @param username - * The name of the user for whom the session is requested. - * @param password - * The password of the user for whom the session is requested - * @param serverPort - * A port at which the Mirth service is listening
- * (OPTIONAL - default: 8443) - * @param serverName - * The name of the Mirth server against which the user account should be validated. The user must exist for the Mirth service at this - * server
- * (OPTIONAL - default: localhost) - * @return The session id or null if session could not be created - * @throws ServiceUnavailableException - */ - public static String createUserSession(String username, String password, Integer serverPort, String serverName) - throws ServiceUnavailableException { - String userSessionCookie = null; - if ((serverName == null) || (serverName.isEmpty())) { - // by default use the Mirth instance that runs MirthMigrator - serverName = defaultServerName; - } - if (serverPort == null) { - // by default use the Mirth standard port - serverPort = defaultServerPort; - } - trustAll(); - // try to authenticate - JSONObject login = login(serverName, serverPort, username, password); - // get the indicator if login was successful - boolean loginSuccessful = login.getBoolean("loginSuccessful"); - // if the login was successful - if (loginSuccessful) { - // get the user session cookie - userSessionCookie = login.getString("sessionCookie").replaceAll(";Path=/api;Secure", ""); - // log the user out again - logout(userSessionCookie, serverPort, serverName); - - synchronized (MirthMigrator.userSessionCache) { - // check if there is still an active session in the cache. This avoids double session timeout in case of concurrent ajax requests - Iterator>> iterator = MirthMigrator.userSessionCache.entrySet().iterator(); - while (iterator.hasNext()) { - // check next session - HashMap session = iterator.next().getValue(); - if (logger.isDebugEnabled()) { - logger.debug("Checking session \"" + session.get("sessionCookie") + "\""); - } - // if the session contains the username and is still valid - if ((session.get("sessionCookie") != null) && session.get("username").equals(username) - && (System.currentTimeMillis() - ((long) session.get("lastAccess")) <= getUserSessionLifeSpan() * 60000)) { - // reset the session life - session.put("lastAccess", System.currentTimeMillis()); - String backup = userSessionCookie; - // and reuse this session - userSessionCookie = (String) session.get("sessionCookie"); - if (logger.isDebugEnabled()) { - logger.debug("Recycled session \"" + userSessionCookie + "\". New session \"" + backup + "\" will be discarded"); - } - // work is done - break; - } - } - } - // store the session cookie (and assure that any pre-existing session of the very user is terminated) - setUserSession(username, userSessionCookie, true); - } - - return userSessionCookie; - } - - /** - * Establishes a mirth client session for a mirth server. In case of success the session cookie is stored in the client instance. - * - * @return true, if the login was successful, false otherwise - * @throws ServiceUnavailableException - */ - private boolean createServerSession() throws ServiceUnavailableException { - - // try to authenticate - JSONObject login = login(getServer(), getPort(), getUsername(), getPassword()); - // get the indicator if login was successful - boolean loginSuccessful = login.getBoolean("loginSuccessful"); - // if the login was successful - if (loginSuccessful) { - // store the session cookie - setServerSessionCookie(login.getString("sessionCookie")); - } - - return loginSuccessful; - } - - /** - * Tries to authenticate a user at a mirth instance and open a session. - * - * @param serverName - * The name of the server the mirth service is running on - * @param Port - * The port under which the mirth service is listening - * @param userName - * The user that should be authenticated - * @param password - * The password of the user that should be authenticated - * @return A JSON object containing the following attributes: - *
    - *
  • responseCode - The HTTP response code
  • - *
  • loginSuccessful - true if the login was successful, false otherwise
  • - *
  • responseMessage - A response message detailing the response status (just in case of failure so far)
  • - *
  • sessionCookie - The cookie identifying the active session
  • - *
- * @throws ServiceUnavailableException - * If the Mirth instance is not available - */ - private static JSONObject login(String serverName, int Port, String userName, String password) throws ServiceUnavailableException { - - JSONObject result = new JSONObject(); - result.put("responseCode", 500); - result.put("responseMessage", "Internal Error - this was not expected"); - result.put("loginSuccessful", false); - - byte[] postDataBytes = null; - - // open the connection - HttpURLConnection urlConnection = connectToRestService(serverName, Port, "/api/users/_login"); - - // create post parameters - Map parameters = new LinkedHashMap<>(); - - try { - parameters.put("username", userName); - parameters.put("password", password); - StringBuilder postData = new StringBuilder(); - // assemble post parameters - for (Map.Entry parameter : parameters.entrySet()) { - if (postData.length() != 0) - postData.append('&'); - postData.append(URLEncoder.encode(parameter.getKey(), "UTF-8")); - postData.append('='); - postData.append(URLEncoder.encode(String.valueOf(parameter.getValue()), "UTF-8")); - } - postDataBytes = postData.toString().getBytes(StandardCharsets.UTF_8); - } catch (UnsupportedEncodingException e2) { - } - - urlConnection.setDoOutput(true); - - try { - urlConnection.setRequestMethod("POST"); - } catch (ProtocolException e1) { - } - urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); - urlConnection.setRequestProperty("Accept", "application/xml"); - - try { - try { - // and also the encoded parameters - urlConnection.getOutputStream().write(postDataBytes); - } catch (SocketTimeoutException e) { - logger.error("Service at " + urlConnection.getURL().getHost() + ":" + urlConnection.getURL().getPort() + " is currently not available: " - + e.getMessage()); - // indicate the unavailable service also in the response - result.put("responseCode", 503); - result.put("loginSuccessful", false); - result.put("responseMessage", - "Service at " + urlConnection.getURL().getHost() + ":" + urlConnection.getURL().getPort() + " is currently not available"); - urlConnection.disconnect(); - return result; - } - - // handle response - result = readResponse(urlConnection); - String responseMessage = result.getString("responseMessage"); - // remember the response code - result.put("responseCode", urlConnection.getResponseCode()); - // set a flag that indicates if the login was successful - Matcher statusMatcher = statusPattern.matcher(responseMessage); - result.put("loginSuccessful", statusMatcher.find() && statusMatcher.group(1).equalsIgnoreCase("SUCCESS")); - // remember the status message, if any (usually just in case of failure) - Matcher messageMatcher = messagePattern.matcher(responseMessage); - result.put("responseMessage", messageMatcher.find() ? statusMatcher.group(1) : ""); - // remember the session cookie if the request was successful - result.put("sessionCookie", - (urlConnection.getResponseCode() == 200) ? urlConnection.getHeaderField("Set-Cookie").replaceAll(";Path=/api;Secure", "") : ""); - } catch (IOException e) {// internal server error (default) will be returned if this happens - } - - try { - // close the connection - urlConnection.disconnect(); - } catch (Exception e) { - // silent close - } - - return result; - } - - /** - * Ends a session with the Mirth service - * - * @param sessionCookie - * The identifier of the session that should be terminated - * @param serverPort - * The port of the Mirth service - * @param serverName - * The name of the server at which the mirth service is located - */ - private static void logout(String sessionCookie, Integer serverPort, String serverName) { - try { - - // open the connection - HttpURLConnection urlConnection = connectToRestService(serverName, serverPort, "/api/users/_logout"); - urlConnection.setUseCaches(false); - urlConnection.setRequestMethod("POST"); - urlConnection.setRequestProperty("X-Requested-With", MirthMigrator.clientIdentifier); - urlConnection.setRequestProperty("Cookie", sessionCookie); - urlConnection.setRequestProperty("Cache-Control", "no-cache"); - - } catch (Exception e) { - logger.error("Exception: " + e.getMessage()); - } - } - - /** - * Provides a timestamp as a human readable date string - * - * @param timestamp - * The date in milliseconds - * @return The date in the following format: dd.MM.yyyy, HH:mm:ss - */ - private static String formatDate(long timestamp) { - return (timestamp > 0) ? displayDate.format(new Date(timestamp)) : "-"; - } - - /** - * Retrieves all relevant information from Mirth. It builds up a new JSONObject, only containing the informations that the client side needs. The - * returned information will be used for populating the tables of the source and the destination system. For the metadata table and the content - * section another function {@link #getComponentDetails(String, String)} will be consumed. - * - * @param groupType - * Either {@link #CHANNEL_GROUP} or {@link #CODE_TEMPLATE_LIBRARY} - * @return A JSON object with the following structure: - *
    - *
  • success - The status that indicates if the operation was successful (true) or not (false)
  • - *
  • statusCode - The HTTP return code (e.g. 200 in case of success)
  • - *
  • payload - the actual payload of the request with the following structure: A JSONObject of channel-groups or code template - * libraries
  • - *
- * If the request was not successful (success = false), the payload usually only consists of an error message - */ - public NativeObject getMetaData(String groupType) { - return getMetaData(groupType, false); - } - - /** - * Retrieves all relevant information from Mirth. It builds up a new JSONObject, only containing the informations that the client side needs. The - * returned information will be used for populating the tables of the source and the destination system. For the metadata table and the content - * section another function {@link #getComponentDetails(String, String)} will be consumed. - * - * @param groupType - * Either {@link #CHANNEL_GROUP} or {@link #CODE_TEMPLATE_LIBRARY} - * @param refresh - * If this flag is set, the configuration of this system will be reloaded before the answer is generated - * @return A JSON object with the following structure: - *
    - *
  • success - The status that indicates if the operation was successful (true) or not (false)
  • - *
  • statusCode - The HTTP return code (e.g. 200 in case of success)
  • - *
  • payload - the actual payload of the request with the following structure: A JSONObject of channel-groups or code template - * libraries
  • - *
- * If the request was not successful (success = false), the payload usually only consists of an error message - */ - public NativeObject getMetaData(String groupType, boolean refresh) { - - // determine group type (channel group or code template library) - boolean isChannelGroup = (CHANNEL_GROUP.equals(groupType)); - - try { - // if a refresh was requested - if (refresh) { - // empty the configuration of this instance - forceRefresh(); - } - - JSONObject metaData = isChannelGroup ? getChannelGroupMetaData() : getCodeTemplateLibraryMetaData(); - return createReturnValue(200, metaData); - - } catch (ConfigurationException e) { - // an invalid configuration was detected - return createReturnValue(500, "Corrupt configuration (either in \"" + configurationFileLocation + "\" or in the " + CHANNEL_GROUP - + " definitions in the Mirth instance \"" + getServer() + "\" itself."); - } catch (ServiceUnavailableException e) { - // the target system is not available - return createReturnValue(503, e.getMessage()); - } - - } - - /** - * Provides meta data information of all channel groups and assigned channels - * - * @return Meta data information of all channel groups and assigned channels - * @throws ServiceUnavailableException - * @throws ConfigurationException - */ - private JSONObject getChannelGroupMetaData() throws ConfigurationException, ServiceUnavailableException { - return getChannelGroupMetaDataById(null); - } - - /** - * Provides meta data information of a specific channel group and assigned channels - * - * @param id - * The id of the desired channel group. (OPTIONAL - If null, the meta data of all groups will be provided) - * @return Meta data information of all or a specific channel group and assigned channels: - * - * - */ - private JSONObject getChannelGroupMetaDataById(String id) throws ConfigurationException, ServiceUnavailableException { - JSONObject metaData = new JSONObject(); - - // no specific id means all code template libraries - if (id == null) { - // get all channel groups in alphabetical order - for (String channelGroupId : getChannelGroupList()) { - // get the meta data for the current channel group - JSONObject currentGroup = getChannelGroupInfoById(channelGroupId); - // and add it to the structure - metaData.accumulate("item", currentGroup); - // now add the meta-data of all child elements - if (currentGroup.has("Members")) { - for (Object channelId : currentGroup.getJSONArray("Members")) { - // add the current element to the structure - metaData.accumulate("item", getChannelMetaDataById((String) channelId)); - } - // indicate how many channels a group possesses - metaData.put("Number of members", currentGroup.get("Number of members")); - } else { - // no channels in this group - metaData.put("Number of members", 0); - } - } - // add info about the total number of groups (the group of unassigned channels is not a real group) - metaData.put("Number of groups", getChannelGroupInfo().size() - 1); - // add info about the total number of channels - metaData.put("Number of members", getChannelInfo().size()); - } else { - // get the metadata for the current code template library - JSONObject currentGroup = getChannelGroupInfoById(id); - // and add it to the structure - metaData.accumulate("item", currentGroup); - - if (currentGroup.has("Members")) { - // now add the meta-data of all child elements - for (Object channelId : currentGroup.getJSONArray("Members")) { - // add the current element to the structure - metaData.accumulate("item", getChannelMetaDataById((String) channelId)); - } - } - // add info about the total number of groups (the group of unassigned channels is not a real group) - metaData.put("Number of groups", (currentGroup.getString("Id").indexOf(' ') < 0) ? 1 : 0); - // add info about the total number of channels - metaData.put("Number of members", currentGroup.get("Number of members")); - } - - // remember the mirth version - metaData.put("Mirth version", getMirthVersion().getVersionString()); - if (metaData.has("item") && (metaData.get("item") instanceof JSONObject)) { - JSONArray itemGroup = new JSONArray().put(metaData.get("item")); - metaData.remove("item"); - metaData.put("item", itemGroup); - } - - return metaData; - } - - /** - * Provides a Mirth client identified by it's name. Mirth clients are cached. If the clients are not cached at the point of time of the request or - * if a reload is explicitly requested, the configuration is loaded and applied.
- *
- * The configuration consists of 3 sections: - *
    - *
  • environment - A list of Mirth environments: - *
      - *
    • id - the unique identifier of the environment
    • - *
    • name - the name of the environment
    • - *
    • color - a color that will be used to show the mirth services that are assigned to this environment
    • - *
    - *
  • - *
  • system - A list of mirth systems: - *
      - *
    • name - The display name for this system
    • - *
    • server - the name or ip that is used to access the system
    • - *
    • environment - a reference to the id of the environment to which the system should be assigned
    • - *
    • description - a description of the system
    • - *
    • user - a username to access the mirth service of the system
    • - *
    • password - the password that correspond to the user
    • - *
    • port - the port that is used to access the system
    • - *
    - *
  • - *
  • excludeFromFunctionDetection - A list of terms that should be excluded from automated function detection
  • - *
- * The configuration file location is defined by {@link #configurationFileLocation}
- *
- * - * @return The Mirth client instance - * @throws IOException - * If the configuration file could not be loaded - * @throws JSONException - * If the configuration file format is invalid (no valid JSON) - * @throws ConfigurationException - * @throws ServiceUnavailableException - */ - private static void loadConfiguration() throws IOException, ConfigurationException, ServiceUnavailableException { - - // truncate caches - MirthMigrator.mirthClients = null; - MirthMigrator.mirthEnvironments = null; - - File configFile = new File(configurationFileLocation); - // check if there is a configuration file - if (!configFile.exists()) { - - // finally indicate that configuration is needed before anything else can be done - throw new ConfigurationException("The Mirth Migrator configuration file \"" + configFile.getAbsolutePath() + "\" is missing."); - } - - // load the configuration file - String rawConfiguration = new String(Files.readAllBytes(configFile.toPath()), StandardCharsets.UTF_8); - - Matcher passwordMatcher = passwordPattern.matcher(rawConfiguration); - StringBuffer decryptedConfig = new StringBuffer(); - - // make sure that all passwords are decrypted in memory - while (passwordMatcher.find()) { - String decryptedPassword = passwordMatcher.group(2); // Extract the password - - try { - // try to decrypt the password - decryptedPassword = decrypt(decryptedPassword); - } catch (Exception e) { - // decryption failed so it seems to be a plain password already - logger.warn("Detected unencrypted password!"); - } - - // replace the plain-text password with the encrypted password - passwordMatcher.appendReplacement(decryptedConfig, passwordMatcher.group(1) + decryptedPassword + passwordMatcher.group(3)); - } - // and add the remaining part - passwordMatcher.appendTail(decryptedConfig); - - // parse the JSON file - JSONObject configuration = new JSONObject(decryptedConfig.toString()); - // cache the configuration - MirthMigrator.configuration = configuration; - - /* 1. Parse the environment configuration */ - - if (!configuration.has("environment")) { - throw new ConfigurationException( - "The configuration file does not contain the \"environment\"-section that defines the integration environments."); - } - - // access the definition of the Mirth systems - JSONArray environments = (configuration.get("environment") instanceof JSONArray) ? configuration.getJSONArray("environment") - : new JSONArray().put(configuration.getJSONObject("environment")); - - // Read in the configuration of all Mirth clients - for (Object element : environments) { - // scan next system configuration - JSONObject environment = (JSONObject) element; - - String environmentId, environmentPosition, environmentName, environmentColor; - - if (!environment.has("id")) { - logger.error("SKIPPING: The Mirth Migrator configuration contains a integration environment definition without id: \n" + environment); - continue; - } - environmentId = environment.getString("id"); - - if (!environment.has("position")) { - logger.error("SKIPPING: The Mirth Migrator configuration contains a integration environment definition without position id: \n" - + environment); - continue; - } - environmentPosition = environment.getInt("position") + ""; - - if (!environment.has("name")) { - logger.error("SKIPPING: The configuration for the integration environment \"" + environmentId - + "\" does not contain the environment name."); - continue; - } - environmentName = environment.getString("name"); - - if (!environment.has("color")) { - logger.error("SKIPPING: The configuration for the integration environment \"" + environmentName - + "\" does not contain the environment color."); - continue; - } - environmentColor = environment.getString("color"); - - // add the environment to the environment cache - addEnvironment(environmentId, environmentPosition, environmentName, environmentColor); - } - - /* 2. Parse the system configuration */ - - if (!configuration.has("system")) { - throw new ConfigurationException("The configuration file does not contain the \"system\"-section that defines the Mirth servers."); - } - - // access the definition of the Mirth systems - JSONArray systems = (configuration.get("system") instanceof JSONArray) ? configuration.getJSONArray("system") - : new JSONArray().put(configuration.getJSONObject("system")); - - // Read in the configuration of all Mirth clients - for (Object element : systems) { - String systemName, server, description, user, password, environment; - int port = 8443; - - // scan next system configuration - JSONObject system = (JSONObject) element; - - if (!system.has("name")) { - logger.error("SKIPPING: The Mirth Migrator configuration contains a system definition without name: \n" + system); - continue; - } - systemName = system.getString("name"); - - if (!system.has("server")) { - logger.error("SKIPPING: The configuration for the system \"" + systemName + "\" does not contain the server address."); - continue; - } - server = system.getString("server"); - - if (!system.has("port")) { - logger.warn("The configuration for the system \"" + systemName - + "\" does not contain the port at which the Mirth service is listening. Using the default port 8443"); - } else { - // if port was not set, use the default port - port = system.getInt("port"); - } - - if (!system.has("environment")) { - logger.error("SKIPPING: The configuration for the system \"" + systemName - + "\" does not contain a reference to the environment to which the system is assigned."); - continue; - } - - environment = system.getString("environment"); - - if (!hasEnvironment(environment)) { - logger.error("SKIPPING: The configuration for the system \"" + systemName + "\" references a Mirth environment called \"" + environment - + "\", which does not exist."); - continue; - } - - // system description is optional - description = system.has("description") ? system.getString("description") : null; - - if (!system.has("user")) { - logger.error("SKIPPING: The configuration for the system \"" + systemName + "\" does not contain a username."); - continue; - } - - user = system.getString("user"); - - if (!system.has("password")) { - logger.error("SKIPPING: The configuration for the system \"" + systemName + "\" does not contain a password."); - continue; - } - - password = system.getString("password"); - - // create the client for the mirth instance - addClient(systemName, environment, server, port, user, password, description); - } - - /* 3. Parse the function filters */ - - // create the filter cache - functionFilter = new ArrayList(); - - // if a filter list was defined - if (configuration.has("excludeFromFunctionDetection")) { - - // access the list of functions to be excluded from function detection - JSONArray excludeFunctions = configuration.getJSONArray("excludeFromFunctionDetection"); - - // and add all functions that should be filtered - for (int index = 0; index < excludeFunctions.length(); index++) { - // add the function to the exclude cache - functionFilter.add(excludeFunctions.getString(index)); - } - } - - /* 4. check for user session life-span configuration */ - - // if a session lifespan was defined - if (configuration.has("sessionLifeSpanInMinutes")) { - int lifeSpan = configuration.getInt("sessionLifeSpanInMinutes"); - if (lifeSpan >= 0) { - // update the session lifespan - setUserSessionLifeSpan(lifeSpan); - logger.debug("Session lifespan has been set to "+getUserSessionLifeSpan()+" minutes"); - } else { - logger.warn("Configured session lifespan of "+lifeSpan+" minutes is invalid. Using default lifespan of "+MirthMigrator.userSessionLifeSpanInMinutes+" minutes"); - } - } else { - logger.warn("Session lifespan was not found in configuration file. Using default lifespan of "+MirthMigrator.userSessionLifeSpanInMinutes+" minutes"); - } - - /* 5. finally, remember the point of time the configuration has been loaded */ - setConfigurationLoadingDate(System.currentTimeMillis()); - } - - private static MirthMigrator addClient(String systemName, String environment, String server, int port, String user, String password, - String description) throws ServiceUnavailableException { - - // if configuration was not yet loaded (or removed to force full reload) - if (MirthMigrator.mirthClients == null) { - // create the Mirth client cache - MirthMigrator.mirthClients = new HashMap(); - } - - // create the client instance - MirthMigrator client = new MirthMigrator(systemName, environment, server, port, user, password, description); - // add it to the cache - mirthClients.put(systemName.toLowerCase(), client); - - // and also provide it as return value - return client; - } - - /** - * Checks if a Mirth client exists - * - * @param systemName - * The name of the system that identifies the Mirth client - * @return true if a Mirth client with the provided system name exists, false otherwise - */ - public static boolean hasClient(String systemName) { - try { - return getClient(systemName) != null; - } catch (Exception e) { - return false; - } - } - - /** - * Provides a Mirth client instance - * - * @param systemName - * The name of the system that identifies the Mirth client - * @return The Mirth client instance or null if the requested instance was not found - * @throws ConfigurationException - * @throws IOException - * If the configuration file could not be loaded - * @throws ServiceUnavailableException - */ - public static MirthMigrator getClient(String systemName) throws IOException, ConfigurationException, ServiceUnavailableException { - return getClient(systemName, false); - } - - /** - * Provides a Mirth client instance - * - * @param systemName - * The name of the system that identifies the Mirth client - * @param forceReload - * If this flag is set, the configuration of the requested client is reloaded - * @return The Mirth client instance or null if the requested instance was not found - * @throws IOException - * If the configuration file could not be loaded - * @throws ConfigurationException - * @throws ServiceUnavailableException - * @throws JSONException - * If the configuration file format is invalid (no valid JSON) - */ - public static MirthMigrator getClient(String systemName, boolean forceReload) - throws IOException, ConfigurationException, ServiceUnavailableException { - // if configuration was not yet loaded (or removed to force full reload) - if (MirthMigrator.mirthClients == null) { - loadConfiguration(); - } - - if ((systemName == null) || systemName.isEmpty()) { - return null; - } - // capitalization must not be of importance - systemName = systemName.toLowerCase(); - - // get the client instance from the cache - MirthMigrator client = mirthClients.get(systemName); - - // if the configuration has changed - if (getConfigLastChange() > getConfigurationLoadingDate()) { - logger.info("Configuration was changed at " + LocalDateTime.ofInstant(Instant.ofEpochMilli(getConfigLastChange()), ZoneId.systemDefault()) - .format(DateTimeFormatter.ofPattern("dd.MM.yyyy, HH:mm:ss.SSS")) + " ==> reloading configuration"); - - // reset the clients list - MirthMigrator.mirthClients = null; - // and reload the configuration - loadConfiguration(); - // indicate to every session that the config has changed and that the app should be reloaded - MirthMigrator.userSessionCache.values().forEach(session -> session.put("configurationChanged", true)); - - // it does not matter that the client might not be up-to-date anymore, as the app will be informed to reload when the session is checked - - } else if (mirthClients.containsKey(systemName) && forceReload) { - // if the client content should be force-updated - // initiate the configuration reload - client.forceRefresh(); - } - - return client; - } - - /** - * Checks if the Mirth Migrator configuration has changed. If the configuration has changed, it will automatically be reloaded once. - * - * @param sessionId - * The id of the session for which it should be checked if the configuration has changed. - * @return true, if the configuration has changed, false otherwise.
- *
- * ATTENTION: This function only returns true once for every session if a configuration has changed. - * Subsequent calls for the same session will return false till the configuration has changed again - * @throws ConfigurationException - * @throws ServiceUnavailableException - * @throws IOException - */ - public static synchronized boolean hasConfigurationChanged(String sessionId) - throws ConfigurationException, IOException, ServiceUnavailableException { - boolean hasChanged = false, needsLoading = false; - Long configurationChangeDate = null; - Long configurationLastLoaded = null; - String configurationChanged = "configurationChanged"; - - configurationChangeDate = getConfigLastChange(); - configurationLastLoaded = getConfigurationLoadingDate(); - - // determine if the configuration needs to be loaded - needsLoading = ((configurationLastLoaded == null) || (configurationChangeDate > configurationLastLoaded)); - - // if the configuration has to be (re)loaded - if (needsLoading) { - // reset the clients list - MirthMigrator.mirthClients = null; - // and (re)load the configuration - loadConfiguration(); - - // determine if configuration has changed (initial loading is ignored here) - hasChanged = (configurationLastLoaded != null) && (configurationChangeDate > configurationLastLoaded); - - // if the configuration has changed, the clients need to be informed - if (hasChanged) { - // indicate to every session that the configuration has changed and that the clients should be reloaded to reflect this change - MirthMigrator.userSessionCache.values().forEach(session -> session.put(configurationChanged, true)); - } - } - - // In case of an already active user session - if (sessionId.startsWith("JSESSIONID")) { - // get the session - HashMap session = getUserSession(sessionId); - // and check if the configurationChanged-flag is set - if ((session != null) && session.containsKey(configurationChanged)) { - // remove the indicator from the session - session.remove(configurationChanged); - // and set the changed flag - hasChanged = true; - } - } else { - // a new session was requested for the user. However it might be that this is just a renewal after a session timeout. This would mean - // the client has to be reloaded nevertheless - // extract the user name from the session id - Matcher credentialSplitMatcher = credentialSplitPattern.matcher(decrypt(sessionId)); - if (credentialSplitMatcher.find()) { - // extract the user name - String username = credentialSplitMatcher.group(1); - - // check if there is an old session for this user. This would mean the user session had a timeout. - Iterator>> iterator = MirthMigrator.userSessionCache.entrySet().iterator(); - while (iterator.hasNext()) { - // check next session - HashMap session = iterator.next().getValue(); - // if the session contains the username and the configurationChanged indicator - if (session.get("username").equals(username) && session.containsKey(configurationChanged)) { - // remove the indicator from the session - session.remove(configurationChanged); - // and set the changed flag - hasChanged = true; - // work is done - break; - } - } - } - } - - return hasChanged; - } - - /** - * Determines the point of time of the last change of the configuration file - * - * @return The point of time of the last change - * @throws ConfigurationException - * If the configuration file is not available - */ - private static long getConfigLastChange() throws ConfigurationException { - - long result = -1; - - Path configFile = Paths.get(configurationFileLocation); - // check if there is a configuration file - if (Files.notExists(configFile)) { - // and indicate if this is not the case - throw new ConfigurationException("The Mirth Migrator configuration file \"" + configFile.toAbsolutePath() + "\" is missing."); - } - - try { - result = Files.readAttributes(configFile, BasicFileAttributes.class).lastModifiedTime().toMillis(); - } catch (IOException e) { - logger.error("Unable to determine last changed date of \"" + configFile.toAbsolutePath() + "\": \n" + e.getMessage()); - } - - return result; - } - - /** - * Creates a new Mirth environment and adds it to the cache.
- *
- * So far environments are only used to differentiate Mirth instances (e.g. production, test, development, fallback) and to assign a specific - * color to the member Mirth instances - * - * @param id - * The identifier of the environment - * @param position - * The position at which the environment should be displayed - * @param name - * The name of the environment - * @param color - * The color of the environment - * @return The new environment - */ - private static HashMap addEnvironment(String id, String position, String name, String color) { - // if configuration was not yet loaded (or removed to force full reload) - if (MirthMigrator.mirthEnvironments == null) { - // create the cache - MirthMigrator.mirthEnvironments = new HashMap>(); - } - - // create the new environment - HashMap environment = new HashMap(); - // add the id - environment.put("id", id); - // add the display position - environment.put("position", position); - // it's name - environment.put("name", name); - // and also the display color - environment.put("color", color); - - // add the new environment to the cache - MirthMigrator.mirthEnvironments.put(id, environment); - - return environment; - } - - /** - * Checks if a Mirth environment exists - * - * @param id - * The id of the Mirth environment that should be checked - * @return True, if the Mirth environment exists, false otherwise - */ - private static boolean hasEnvironment(String id) { - try { - return getEnvironment(id) != null; - } catch (Exception e) { - return false; - } - } - - /** - * Get a Mirth environment from cache - * - * @param id - * The id of the requested Mirth environment - * @return The requested Mirth environment or null if it was not found - * @throws IOException - * @throws ConfigurationException - * @throws ServiceUnavailableException - */ - private static HashMap getEnvironment(String id) throws ConfigurationException, IOException, ServiceUnavailableException { - - // if configuration was not yet loaded (or removed to force full reload) - if (MirthMigrator.mirthEnvironments == null) { - loadConfiguration(); - } - // get the Mirth environment from the cache - return mirthEnvironments.get(id); - } - - /** - * Creates a hash value from a string - * - * @param string - * the string from which the hash value should be created - * @return the hash value as string (not as hex string) or null if the conversion was not possible - */ - private static String createHash(String string) { - String checksum = null; - - try { - // SHA-256 should be sufficient - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - // create the hash value - byte[] hashBytes = digest.digest(string.getBytes(StandardCharsets.UTF_8)); - - // and now - StringBuilder hexString = new StringBuilder(); - // transform every byte - for (byte hashByte : hashBytes) { - // to it's corresponding hex string - String hex = Integer.toHexString(0xff & hashByte); - if (hex.length() == 1) - hexString.append('0'); - // and combine it with the other bytes - hexString.append(hex); - } - // set the return value - checksum = hexString.toString(); - - } catch (NoSuchAlgorithmException e) { - // should never happen - logger.error("Unable to generate checksum: " + e.getMessage()); - } - - return checksum; - } - - /** - * Provides an ordered list of all channel group IDs - * - * @return A collection of channel group IDs - * @throws ServiceUnavailableException - * @throws ConfigurationException - */ - private Collection getChannelGroupList() throws ConfigurationException, ServiceUnavailableException { - if (this.channelGroupInfo == null) { - // it is important to first generate the information before they are provided to the user - getChannelGroupInfo(); - } - - return this.channelGroupOrder.values(); - } - - /** - * Provides the metadata of a specific channel group identified by it's unique id.
- * This does not include the metadata of the contained channels.
- *
- * This is is needed, the function {@link #getChannelGroupMetaDataById(String)} should be called - * - * @param channelGroupId - * The channel group id - * @return The channel group meta data - * @throws ServiceUnavailableException - * @throws ConfigurationException - */ - private JSONObject getChannelGroupInfoById(String channelGroupId) throws ConfigurationException, ServiceUnavailableException { - return getChannelGroupInfo().get(channelGroupId); - } - - /** - * Provides a map containing metadata of all channel groups and their channels
- * - * - * @return A HashMap identifying each group by it's id and providing the following information per group: - *
    - *
  • Display name - The name of the channel group
    - * For the default group this value will always be "Unassigned Channels"
  • - *
  • Id - The UUID of the channel group.
    - * For the default group this value will always be "Unassigned Channels"
  • - *
  • Description - A description of the purpose of this channel group
  • - *
  • Last modified - Timestamp of the last modification
  • - *
  • Display date - A human readable representation of the Last modified timestamp in the format dd.MM.yyyy, - * HH:mm:ss
  • - *
  • Version - The version of the channel group. Every change increments the version number by 1.
  • - *
  • Mirth version - The version of the Mirth system for which the channel group has been deployed
  • - *
  • Group - indicates that it is a grouping element (value is always true)
  • - *
  • Type - channelGroup
  • - *
  • Members - Lists all channels that belong to this channel group with the following attributes: - *
      - *
    • id - The UUID of the channel
    • - *
    • Display name - The name of the channel
    • - *
    - *
  • - *
  • Number of members - The number of channel that belong to this channel group
  • - *
  • artificial - is set to true, if it is the default group (which actually is not really a group) and false, otherwise
  • - *
- * @throws ServiceUnavailableException - * @throws ConfigurationException - */ - private HashMap getChannelGroupInfo() throws ConfigurationException, ServiceUnavailableException { - // lazy fetching - if (this.channelGroupInfo == null) { - - HttpURLConnection service = null; - // either code-template libraries or channel groups - JSONObject raw = null; - JSONObject currentGroup = null; - JSONArray groups = null; - String mirthVersion = null; - - HashSet assignedChannels = new HashSet(); - this.channelGroupInfo = new HashMap(); - this.channelGroupOrder = new TreeMap(); - - // 1.) retrieve the group structure and harmonize it if necessary - service = connectToRestService("/api/channelgroups"); - raw = getResponseAsJson(service); - - if (raw != null) { - // make sure that it is always an array - even if only one channel group was provided - groups = (raw.get(CHANNEL_GROUP) instanceof JSONArray) ? raw.getJSONArray(CHANNEL_GROUP) - : new JSONArray().put(raw.getJSONObject(CHANNEL_GROUP)); - - // 2.) fetch meta data for all groups - - // extract the version of the mirth system from the first channel - if (mirthVersion == null) { - mirthVersion = groups.getJSONObject(0).getString("version"); - } - - // collect meta data for all channel groups - for (Object element : groups) { - // scan next group - currentGroup = (JSONObject) element; - // a new jsonObject to put the groups main attributes - JSONObject metaData = new JSONObject(); - - // add the group name - metaData.accumulate("Display name", currentGroup.getString("name")); - // add the group id - metaData.accumulate("Id", currentGroup.getString("id")); - // indicate that this group is not artificial - metaData.accumulate("artificial", false); - // add the description of the channel purpose - metaData.accumulate("Description", currentGroup.getString("description").replaceAll("<", "<").replaceAll(">", ">") - .replaceAll("\\r\\n|\\r|\\n", "
")); - // add the last modified date for sorting - metaData.accumulate("Last modified", currentGroup.getJSONObject("lastModified").getLong("time")); - // add the last modified date for displaying - metaData.accumulate("Display date", formatDate(metaData.getLong("Last modified"))); - // add the revision id - metaData.accumulate("Version", currentGroup.get("revision")); - // add the revision id - metaData.accumulate("Mirth version", mirthVersion); - // indicate that it is a grouping element - metaData.accumulate("Group", true); - // not yet sure for what the item type is needed - metaData.accumulate("Type", CHANNEL_GROUP); - // 3.) Add ordered references to channels - TreeMap groupMemberOrder = new TreeMap(); - // tag does not exist if group is empty - try { - // get harmonized reference to all channels of a group - JSONArray groupMembers = (currentGroup.getJSONObject("channels").get("channel") instanceof JSONArray) - ? currentGroup.getJSONObject("channels").getJSONArray("channel") - : new JSONArray().put(currentGroup.getJSONObject("channels").getJSONObject("channel")); - - // now order all channels of the channel group by name - for (Object member : groupMembers) { - // get the reference to the channel - String reference = ((JSONObject) member).getString("id"); - // if the referenced channel actually exists - if (getChannelInfo().containsKey(reference)) { - // add it to the ordered map with its name as key - groupMemberOrder.put(getChannelInfo().get(reference).getString("Display name").toLowerCase(), reference); - // remember that this channel has been assigned to a group - assignedChannels.add(reference); - } else { - logger.error("The channel group \""+currentGroup.getString("name")+"\" references a channel with id \""+reference+"\" that does not exist."); - } - } - - // add information about the number of channels in this group - metaData.accumulate("Number of members", groupMemberOrder.size()); - // add the ordered list of references to the channel group meta data - metaData.put("Members", groupMemberOrder.values()); - - } catch (JSONException ex) { - // this group does not possess any channels - metaData.accumulate("Number of members", 0); - } - - // 4.) add channel group to the ordered list - this.channelGroupOrder.put(metaData.getString("Display name").toLowerCase(), metaData.getString("Id")); - // and also to the cache - this.channelGroupInfo.put(metaData.getString("Id"), metaData); - } - } - - // 5.) generate a default group and add all unassigned channels - JSONObject metaData = new JSONObject(); - - // add the group name - metaData.accumulate("Display name", "Unassigned Channels"); - // add the group id - metaData.accumulate("Id", "Unassigned Channels"); - // indicate that this group is artificial - metaData.accumulate("artificial", true); - // add the description of the channel purpose - metaData.accumulate("Description", "All channels that have not yet been assigned to a group."); - // add the last modified date for sorting - metaData.accumulate("Last modified", null); - // add the last modified date for displaying - metaData.accumulate("Display date", "-"); - // add the revision id - metaData.accumulate("Version", "1"); - // add the revision id - metaData.accumulate("Mirth version", mirthVersion); - // indicate that it is a grouping element - metaData.accumulate("Group", true); - // not yet sure for what the item type is needed - metaData.accumulate("Type", CHANNEL_GROUP); - - // now scan for channels that have not been assigned to a channel group - TreeMap groupMemberOrder = new TreeMap(); - for (String currentChannelId : getChannelInfo().keySet()) { - if (assignedChannels.contains(currentChannelId)) { - continue; - } - groupMemberOrder.put(getChannelInfo().get(currentChannelId).getString("Display name").toLowerCase(), currentChannelId); - } - // add information about the number of channels in this group - metaData.accumulate("Number of members", groupMemberOrder.size()); - // add the ordered list of references to the "Unassigned channels" group meta data - metaData.put("Members", groupMemberOrder.values()); - - // add the artificial channel group to the ordered list - this.channelGroupOrder.put(metaData.getString("Display name").toLowerCase(), metaData.getString("Id")); - // and also to the cache - this.channelGroupInfo.put(metaData.getString("Id"), metaData); - - // update the update indicator - this.lastUpdate = System.currentTimeMillis(); - } - - return this.channelGroupInfo; - } - - /** - * Provides the metadata of a specific channel identified by it's id - * - * @param channelId - * The unique channel id - * @return The channel meta data - * @throws ServiceUnavailableException - * @throws ConfigurationException - */ - private JSONObject getChannelMetaDataById(String channelId) throws ConfigurationException, ServiceUnavailableException { - return getChannelInfo().get(channelId); - } - - /** - * Provides meta data information of all code template libraries and assigned code templates - * - * @return Meta data information of all code template libraries and assigned code templates - * @throws ServiceUnavailableException - */ - private JSONObject getCodeTemplateLibraryMetaData() throws ServiceUnavailableException { - return getCodeTemplateLibraryMetaDataById(null); - } - - /** - * Provides meta data information of a specific code template library and assigned code templates - * - * @param id - * The id of the desired code template library. If null, the meta data of all libraries will be provided - * @return Meta data information of a specific code template libraries and assigned code templates - * @throws ServiceUnavailableException - * @throws JSONException - */ - private JSONObject getCodeTemplateLibraryMetaDataById(String id) throws ServiceUnavailableException { - JSONObject metaData = new JSONObject(); - - // no specific id means all code template libraries - if (id == null) { - // get all code template libraries in alphabetical order - for (String codeTemplateLibraryId : getCodeTemplateLibraryList()) { - // get the metadata for the current code template library - JSONObject currentGroup = getCodeTemplateLibraryInfoById(codeTemplateLibraryId); - // and add it to the structure - metaData.accumulate("item", currentGroup); - - // now add the metadata of all child elements - if (currentGroup.has("Members")) { - for (Object codeTemplateId : currentGroup.getJSONArray("Members")) { - // add the current element to the structure - metaData.accumulate("item", getCodeTemplateMetaDataById((String) codeTemplateId)); - } - } - } - // add info about the total number of groups (the group of unassigned channels is not a real group) - metaData.put("Number of groups", getCodeTemplateLibraryInfo().size()); - // add info about the total number of channels - metaData.put("Number of members", getCodeTemplateInfo().size()); - } else { - // get the metadata for the current code template library - JSONObject currentGroup = getCodeTemplateLibraryInfoById(id); - // and add it to the structure - metaData.accumulate("item", currentGroup); - - // now add the metadata of all child elements - if (currentGroup.has("Members")) { - for (Object codeTemplateId : currentGroup.getJSONArray("Members")) { - // add the current element to the structure - metaData.accumulate("item", getCodeTemplateMetaDataById((String) codeTemplateId)); - } - } - - // add info about the total number of groups, which is always 1 in this case - metaData.put("Number of groups", 1); - // add info about the total number of channels - metaData.put("Number of members", currentGroup.getJSONArray("Members").length()); - } - - // if the is no channel from which it can be taken, fetch it from the server via the api (expensive!) - metaData.put("Mirth version", getMirthVersion().getVersionString()); - - // make sure that attribute item is always a JSON array - if (metaData.has("item")) { - if (metaData.get("item") instanceof JSONObject) { - // transform the only item to an array - JSONArray itemGroup = new JSONArray().put(metaData.get("item")); - metaData.remove("item"); - metaData.put("item", itemGroup); - } - } else { - // there are no items. Use an empty array - metaData.put("item", new JSONArray()); - } - - return metaData; - } - - /** - * Provides an ordered list of all code template libraries - * - * @return A collection of code template library ids - * @throws ServiceUnavailableException - */ - private Collection getCodeTemplateLibraryList() throws ServiceUnavailableException { - if (this.codeTemplateLibraryInfo == null) { - // it is important to first generate the information before they are provided to the user - getCodeTemplateLibraryInfo(); - } - - return this.codeTemplateLibraryOrder.values(); - } - - /** - * Provides a mapping between functions and all channels that reference this function - * - * @return The mapping between functions and the channels that reference these functions - * @throws ServiceUnavailableException - * @throws ConfigurationException - */ - private HashMap> getChannelReferencesToFunction() throws ConfigurationException, ServiceUnavailableException { - if (this.channelReferencesToFunction == null) { - // load channel information as this also parses channels for function references - getChannelInfo(); - } - - return this.channelReferencesToFunction; - } - - /** - * Forces the Mirth client instance to reload the channel, code template, channel group, and code template library configuration from the server - * - * @throws ServiceUnavailableException - */ - private void forceRefresh() throws ServiceUnavailableException { - // empty the configuration of this instance - this.channelCodeTemplateLibraryReferences = null; - this.channelFunctionReferences = null; - this.channelGroupInfo = null; - this.channelGroupOrder = null; - this.channelInfo = null; - this.channelInternalFunctionsByChannelId = null; - this.channelLastModified = null; - this.channelReferencesToFunction = null; - this.channelState = null; - this.channelIdbyName = null; - this.channelNameById = null; - this.codeTemplateIdToFunction = null; - this.codeTemplateLibraryIdByCodeTemplateId = null; - this.codeTemplateInfo = null; - this.codeTemplateLibraryInfo = null; - this.codeTemplateLibraryOrder = null; - this.codeTemplateIdbyName = null; - this.codeTemplateNameById = null; - this.functionLinkedByFunctions = null; - this.codeTemplateIdByFunctionName = null; - this.functionUsesFunctions = null; - this.externalResources = null; - this.interChannelDependencies = null; - this.functionConflicts = null; - this.unknownChannelFunctions = null; - this.unknownFunctionFunctions = null; - - /* - if(logger.isDebugEnabled()) { - StringWriter stackTrace = new StringWriter(); - PrintWriter printWriter = new PrintWriter(stackTrace); - - // Capture the current stack trace - new Exception().printStackTrace(printWriter); - - logger.debug("forceRefresh(" + getServer() + ") was called from: \n" + stackTrace.toString()); - } - */ - } - - /** - * Provides the metadata of a specific code template library identified by it's unique id.
- * This does not include the metadata of the contained code templates.
- *
- * This is is needed, the function {@link #getCodeTemplateLibraryMetaDataById(String)} should be called - * - * @param codeTemplateLibraryId - * The code template library id - * @return The code template library meta data - * @throws ServiceUnavailableException - */ - private JSONObject getCodeTemplateLibraryInfoById(String codeTemplateLibraryId) throws ServiceUnavailableException { - return getCodeTemplateLibraryInfo().get(codeTemplateLibraryId); - } - - /** - * Provides a map containing metadata of all code template libraries. If it does not exist, it will be generated.
- *
- * It consists of the following information:
- *
    - *
  • Name - The name of the channel group
  • - *
  • Id - The UUID of the channel group
  • - *
  • Description - A description of the code template
  • - *
  • Version - The version of the channel group. Every change increases version number by 1.
  • - *
  • Group - Indicates that it is a grouping element
  • - *
  • Last modified - Timestamp of the last modification of the code template library
  • - *
  • Display date - A human readable representation of the Last modified timestamp
  • - *
  • Used by - A list of ids of channels that reference this code template library
  • - *
  • Number of code templates - The number of code templates the library comprises
  • - *
  • Code templates - An ordered list of ids of code templates that belong to this code template library
  • - *
- * - * @return A hashmap with meta data of all code template libraries identified by their id - * @throws ServiceUnavailableException - */ - private HashMap getCodeTemplateLibraryInfo() throws ServiceUnavailableException { - - // lazy fetching - if (this.codeTemplateLibraryInfo == null) { - HttpURLConnection service = null; - // either code-template libraries or channel groups - JSONObject raw = null; - JSONObject currentGroup = null; - JSONArray groups = null; - - // 1.) retrieve the group structure and harmonize it if necessary - service = connectToRestService("/api/codeTemplateLibraries"); - raw = getResponseAsJson(service); - - this.codeTemplateLibraryInfo = new HashMap(); - this.codeTemplateLibraryOrder = new TreeMap(); - this.channelCodeTemplateLibraryReferences = new HashMap>(); - this.codeTemplateLibraryIdByCodeTemplateId = new HashMap(); - this.codeTemplateIdByFunctionName = new HashMap(); - - // if there are no code template libraries at the Mirth instance - if (raw == null) { - // no more work has to be done - return this.codeTemplateLibraryInfo; - } - - // if it is an object instead of an array (meaning only 1 code template library) - groups = (raw.get(CODE_TEMPLATE_LIBRARY) instanceof JSONArray) ? raw.getJSONArray(CODE_TEMPLATE_LIBRARY) - : new JSONArray().put(raw.getJSONObject(CODE_TEMPLATE_LIBRARY)); - - // 2.) fetch meta data for all code template library - - // extract the version of the mirth system from the first channel - String mirthVersion = getMirthVersion().getVersionString(); - - // collect meta data for all code template libraries - for (Object element : groups) { - // scan next group - currentGroup = (JSONObject) element; - // a new jsonObject to put the groups main attributes - JSONObject metaData = new JSONObject(); - - // add the code template library name - metaData.accumulate("Display name", currentGroup.getString("name")); - // add the code template library id - String libraryId = currentGroup.getString("id"); - metaData.accumulate("Id", libraryId); - // add the description of the code template library purpose - metaData.accumulate("Description", currentGroup.getString("description").replaceAll("<", "<").replaceAll(">", ">") - .replaceAll("\\\"", """).replaceAll("\\r|\\n", "
")); - // add the last modified date for sorting - metaData.accumulate("Last modified", currentGroup.getJSONObject("lastModified").getLong("time")); - // add the last modified date for displaying - metaData.accumulate("Display date", formatDate(metaData.getLong("Last modified"))); - // add the revision id - metaData.accumulate("Version", currentGroup.getInt("revision")); - // add the revision id - metaData.accumulate("Mirth version", mirthVersion); - // indicate that it is a grouping element - metaData.accumulate("Group", true); - // not sure for what the item type is needed - metaData.accumulate("Type", CODE_TEMPLATE_LIBRARY); - - // 3.) references to channels - if (currentGroup.get("enabledChannelIds") instanceof JSONObject) { - - // get harmonized reference to all code templates of a code template library - JSONArray referencingChannels = (currentGroup.getJSONObject("enabledChannelIds").get("string") instanceof JSONArray) - ? currentGroup.getJSONObject("enabledChannelIds").getJSONArray("string") - : new JSONArray().put(currentGroup.getJSONObject("enabledChannelIds").getString("string")); - - // add the ordered list of referencing channels - metaData.put("Used by", referencingChannels); - - // build up an index of libraries used per channel - for (Object id : referencingChannels) { - // get the next channel reference - String channelId = (String) id; - // if there is not yet an entry for the channel - if (!this.channelCodeTemplateLibraryReferences.containsKey(channelId)) { - // generate one - this.channelCodeTemplateLibraryReferences.put(channelId, new ArrayList(1)); - } - - // add library reference to the channel - ArrayList references = this.channelCodeTemplateLibraryReferences.get(channelId); - references.add(libraryId); - } - } - - // 4.) Add ordered references to code templates - TreeMap groupMemberOrder = new TreeMap(); - try { - // get harmonized reference to all code templates of a code template library - JSONArray groupMembers = (currentGroup.getJSONObject("codeTemplates").get("codeTemplate") instanceof JSONArray) - ? currentGroup.getJSONObject("codeTemplates").getJSONArray("codeTemplate") - : new JSONArray().put(currentGroup.getJSONObject("codeTemplates").getJSONObject("codeTemplate")); - - // now order all templates of the library by name - for (Object member : groupMembers) { - - // get the reference to the code template - String codeTemplateId = ((JSONObject) member).getString("id"); - // and also the function name - String functionName = getCodeTemplateMetaDataById(codeTemplateId).getString("Function name"); - // and add it to the ordered map with its name as key - groupMemberOrder.put(functionName.toLowerCase(), codeTemplateId); - // create mapping from function to library - this.codeTemplateLibraryIdByCodeTemplateId.put(codeTemplateId, libraryId); - - int counter = 2; - String functionId = null; - while (getCodeTemplateInfo().containsKey(codeTemplateId + "_" + counter)) { - // create the function id - functionId = codeTemplateId + "_" + counter++; - // get the function name - functionName = getCodeTemplateMetaDataById(functionId).getString("Function name"); - // add it to the order list - groupMemberOrder.put(functionName.toLowerCase(), functionId); - // create mapping from function to library - this.codeTemplateLibraryIdByCodeTemplateId.put(functionId, libraryId); - - // and also one from function name to code template - this.codeTemplateIdByFunctionName.put(functionName, codeTemplateId); - } - } - - // add information about the number of templates in this group - metaData.accumulate("Number of members", groupMemberOrder.size()); - // add the ordered list of references to the code template library meta data - metaData.put("Members", groupMemberOrder.values()); - } catch (JSONException ex) { - // this group does not possess any code template - metaData.accumulate("Number of members", 0); - } - - // 5.) add code template library to the ordered list - this.codeTemplateLibraryOrder.put(metaData.getString("Display name").toLowerCase(), metaData.getString("Id")); - // and also to the cache - this.codeTemplateLibraryInfo.put(libraryId, metaData); - } - // update the update indicator - this.lastUpdate = System.currentTimeMillis(); - } - - - return this.codeTemplateLibraryInfo; - } - - /** - * Checks if there is a conflict for this function. The conflict itself can be obtained via getFunctionConflicts() - * @param functionName The name of the function that should be checked - * @param codeTemplateId The id of the code template against which the function should be checked - * @return True, if there is a conflict; false otherwise - * @throws JSONException - * @throws ServiceUnavailableException - */ - private boolean checkForFunctionConflicts(String functionName, String codeTemplateId) throws JSONException, ServiceUnavailableException { - - // if there is already a link to a code template for this function it means the function is defined multiple times - if((this.codeTemplateIdByFunctionName!= null) && (this.codeTemplateIdByFunctionName.containsKey(functionName))) { - // as javascript does not support function overloading by defining a function multiple times w/ differing parameter sets, we might have an issue here - // first check if there is already a valid container for function conflicts - if(this.functionConflicts == null) { - // nope, create it - this.functionConflicts = new HashMap>(); - } - - // check if there is not yet a conflict record for this function - if(!this.functionConflicts.containsKey(functionName)) { - // create a new entry - HashMap newEntry = new HashMap(); - // add the initial code template - newEntry.put(getCodeTemplateNameById(this.codeTemplateIdByFunctionName.get(functionName)), 1); - // add the entry to the conflict list - functionConflicts.put(functionName, newEntry); - } - - // get the record for this function - HashMap conflictRecord = functionConflicts.get(functionName); - // and also the name of the code template - String codeTemplateName = getCodeTemplateNameById(codeTemplateId); - - // add a detection for this code template (a function might be defined multiple times on the same code template) - conflictRecord.put(codeTemplateName, conflictRecord.containsKey(codeTemplateName) ? conflictRecord.get(codeTemplateName) + 1 : 1); - for (Map.Entry entry : functionConflicts.get(functionName).entrySet()) { - String codeTemplateNameReference = entry.getKey(); - Integer amount = entry.getValue(); - } - return true; - } - - return false; - } - - /** - * Provides the conflicts of a function - * @param functionName The name of the function - * @return a HashMap containing the conflicting code templates and the number of times the function is defined in the respective code template - * @throws ServiceUnavailableException - */ - private HashMap getFunctionConflicts(String functionName) throws ServiceUnavailableException{ - - return (this.functionConflicts != null) ? this.functionConflicts.get(functionName) : null; - } - - /** - * Provides the metadata of a specific code template identified by it's id - * - * @param codeTemplateId - * The unique code template id - * @return The code template meta data - * @throws ServiceUnavailableException - */ - private JSONObject getCodeTemplateMetaDataById(String codeTemplateId) throws ServiceUnavailableException { - return getCodeTemplateInfo().get(codeTemplateId); - } - - /** - * Provides a map containing metadata of all code templates. If it does not exist, it will be generated. - * - * @return A HashMap with meta data of all code templates identified by their id. The value consists of a JSON object containing the following - * information:
- *
    - *
  • Display name - The display name that has been configured for the code template
  • - *
  • Function name - The name of the actual function if the code template is a function. Otherwise the same like Display - * name
  • - *
  • Id - The UUID of the code template
  • - *
  • Version - The version of the code template. Every change increases version number by 1.
  • - *
  • Last modified - The timestamp of the last modification of the code template
  • - *
  • Display date - A human readable representation of the Last modified timestamp
  • - *
  • Description - A description of the code template
  • - *
  • Parameters - A description of the parameters
  • - *
  • Return value - A description of the return value
  • - *
  • Is function - True, if code template is a function
  • - *
- * @throws ServiceUnavailableException - */ - private synchronized HashMap getCodeTemplateInfo() throws ServiceUnavailableException { - // lazy fetching - if (this.codeTemplateInfo == null) { - // initialize container - this.codeTemplateInfo = new HashMap(); - // and also the name to template mapper - this.codeTemplateIdbyName = new HashMap(); - this.codeTemplateNameById = new HashMap(); - this.codeTemplateIdToFunction = new HashMap>(); - - // get info about all code templates - String xml = getResponseAsXml(connectToRestService("/api/codeTemplates")); - // scan the channel code for code template usage - buildUpTemplateToTemplateRelationships(xml); - // and prepare it for metadata parsing - JSONObject raw = XML.toJSONObject(xml); - try { - raw = raw.getJSONObject("list"); - } catch (JSONException e) { - // if the library is empty, there will be no code template information - return this.codeTemplateInfo; - } - - // and extract the relevant information of each - JSONArray codeTemplates = (raw.get("codeTemplate") instanceof JSONArray) ? raw.getJSONArray("codeTemplate") : (new JSONArray()).put(raw.get("codeTemplate")); - for (Object element : codeTemplates) { - // get next code template - JSONObject codeTemplate = (JSONObject) element; - - // arm the matcher to check for function definitions within the code template - Matcher functionNameMatcher = functionNamePattern.matcher(codeTemplate.getJSONObject("properties").getString("code")); - // if a function definition is found, the code template contains at least one function - if (functionNameMatcher.find()) { - String codeTemplateId = codeTemplate.getString("id"); - String functionName = functionNameMatcher.group(1) + "()"; - - // initialize the cache entry - this.codeTemplateIdToFunction.put(codeTemplateId, new HashSet()); - // add metadata for the first function of the code template - generateCodeTemplateMetaData(codeTemplate, functionName); - // cache the reference between code template and function - this.codeTemplateIdToFunction.get(codeTemplateId).add(functionName); - - // add metadata for each function within the code template - int index = 2; - while (functionNameMatcher.find()) { - functionName = functionNameMatcher.group(1) + "()"; - // add metadata for the function - generateCodeTemplateMetaData(codeTemplate, functionName, index++); - // cache the reference between code template and function - this.codeTemplateIdToFunction.get(codeTemplateId).add(functionName); - } - } else { - // code template does not contain any function definitions. Create meta data anyway - generateCodeTemplateMetaData(codeTemplate); - } - } - // update the update indicator - this.lastUpdate = System.currentTimeMillis(); - } - - return this.codeTemplateInfo; - } - - /** - * Provides a list of inter-function-references - * - * @return A list of functions referenced by functions - * @throws ServiceUnavailableException - */ - private HashMap> getFunctionUsesFunctions() throws ServiceUnavailableException { - if (this.functionUsesFunctions == null) { - getCodeTemplateInfo(); - } - - return this.functionUsesFunctions; - } - - - /** - * Generates metadata for a code template that is no function. It consists of the following information:
- *
    - *
  • Display name
    - * The display name that has been configured for the code template
  • - *
  • Function name
    - * The same like Display name as this code template is no function
  • - *
  • Id
    - * The UUID of the code template
  • - *
  • Version
    - * The version of the code template. Every change increases version number by 1.
  • - *
  • Last modified
    - * Timestamp of the last modification of the code template
  • - *
  • Display date
    - * A human readable representation of the Last modified timestamp
  • - *
  • Description
    - * A description of the code template
  • - *
  • Is function
    - * always false, as this code template is no function
  • - *
- * - * @param codeTemplate - * The code template for which the metadata should be generated - * @throws ServiceUnavailableException - * - */ - private void generateCodeTemplateMetaData(JSONObject codeTemplate) throws ServiceUnavailableException { - generateCodeTemplateMetaData(codeTemplate, null, null); - } - - /** - * Generates metadata for a code template. It consists of the following information: - *
    - *
  • Display name - The display name that has been configured for the code template
  • - *
  • Function name - The name of the actual function if the code template is a function. Otherwise the same like Display name
  • - *
  • Id - The UUID of the code template
  • - *
  • Version - The version of the code template. Every change increases version number by 1.
  • - *
  • Last modified - Timestamp of the last modification of the code template
  • - *
  • Display date - A human readable representation of the Last modified timestamp
  • - *
  • Description - A description of the code template
  • - *
  • Parameters - A description of the parameters
  • - *
  • Return value - A description of the return value
  • - *
  • Is function - True, if code template is a function
  • - *
- * - * @param codeTemplate - * The code template for which the metadata should be generated - * @param functionName - * The name of the function or null if it is no function - * @throws ServiceUnavailableException - */ - private void generateCodeTemplateMetaData(JSONObject codeTemplate, String functionName) throws ServiceUnavailableException { - generateCodeTemplateMetaData(codeTemplate, functionName, null); - } - - /** - * Generates metadata for a code template. It consists of the following information: - *
    - *
  • Display name - The display name that has been configured for the code template
  • - *
  • Function name - The name of the actual function if the code template is a function. Otherwise the same like Display name
  • - *
  • Id - The UUID of the code template
  • - *
  • Version - The version of the code template. Every change increases version number by 1.
  • - *
  • Last modified - Timestamp of the last modification of the code template
  • - *
  • Display date - A human readable representation of the Last modified timestamp
  • - *
  • Description - A description of the code template
  • - *
  • Parameters - A description of the parameters
  • - *
  • Return value - A description of the return value
  • - *
  • Is function - True, if code template is a function
  • - *
- * - * @param codeTemplate - * The code template for which the metadata should be generated - * @param functionName - * The name of the function or null if it is no function - * @param index - * An index that will be added to the code template id if there are more than 1 function in a code template. If index is null, none - * will be added - * @throws ServiceUnavailableException - */ - private void generateCodeTemplateMetaData(JSONObject codeTemplate, String functionName, Integer index) throws ServiceUnavailableException { - // create a new element - JSONObject metaData = new JSONObject(); - // add the configured name of the code template - metaData.accumulate("Display name", codeTemplate.getString("name")); - // add the function name of the code template. If it is no function use the template name - metaData.accumulate("Function name", (functionName != null) ? functionName : metaData.getString("Display name")); - // add the id of the code template. If there are more than one functions in a code template, create an artificial id (that allows to - // reconstruct the original id) - String codeTemplateId = codeTemplate.getString("id") + ((index != null) ? "_" + index.intValue() : ""); - metaData.accumulate("Id", codeTemplateId); - // add the version of the code template - metaData.accumulate("Version", codeTemplate.get("revision")); - // not yet sure for what the item type is needed - metaData.accumulate("Type", CODE_TEMPLATE); - - if (codeTemplate.getJSONObject("lastModified") != null) { - // add the last modified date for sorting - metaData.accumulate("Last modified", codeTemplate.getJSONObject("lastModified").get("time")); - // add the last modified date for displaying - metaData.accumulate("Display date", formatDate(metaData.getLong("Last modified"))); - } else { - // no modified date - metaData.accumulate("Last modified", "-"); - // no modified date - metaData.accumulate("Display date", "-"); - } - - JSONObject header = getCodeTemplateDescription(codeTemplate, functionName); - - // if there is a JavaScript Doc header for this function, add the available details - if (header != null) { - // if there is a description of the function - if (header.has("description")) { - // add the description of the code template - metaData.accumulate("Description", header.getString("description")); - } - // if the function possesses parameters - if (header.has("parameters")) { - // add the parameters of the code template - metaData.accumulate("Parameters", header.getString("parameters")); - } - // if the function provides a return value - if (header.has("returnValue")) { - // add the return value of the code template - metaData.accumulate("Return value", header.getString("returnValue")); - } - } - boolean isFunction = (functionName != null); - // indicate if code template is contains functions - metaData.accumulate("Is function", isFunction); - - // add the mapping to the metadata HashMap - this.codeTemplateInfo.put(codeTemplateId, metaData); - - // as well as the mapping between the template name and the id - getCodeTemplateIdByName().put(metaData.getString("Display name"), codeTemplateId); - getCodeTemplateNameById().put(codeTemplateId, metaData.getString("Display name")); - - // if it is a function - if (isFunction) { - // also add a mapping between function name and code template id - getCodeTemplateIdByName().put(metaData.getString("Function name"), codeTemplateId); - - // check if the function has conflicts - if (checkForFunctionConflicts(functionName, codeTemplateId)) { - // it has. Get the list of conflicts for this function - HashMap allConflicts = getFunctionConflicts(functionName); - - // and assemble the display list of conflicting elements - ArrayList multipleDefinitions = new ArrayList(); - for (Map.Entry entry : allConflicts.entrySet()) { - String conflictingCodeTemplate = entry.getKey(); - Integer numberOfDefinitions = entry.getValue(); - multipleDefinitions.add(conflictingCodeTemplate + ((numberOfDefinitions != 1) ? " (" + numberOfDefinitions + "x)" : "")); - } - - // first check if there is already an issue attribute for this channel - if (!metaData.has("Issues")) { - // if not, create it - metaData.put("Issues", new JSONObject()); - } - // add the list of multiple definitions of the same function - metaData.getJSONObject("Issues").put("multipleDefinitions", multipleDefinitions); - } - } - - // and also one from function name to code template - this.codeTemplateIdByFunctionName.put(functionName, codeTemplateId); - } - - /** - * Provides a HashMap containing the header information of all functions detected in a code template - * - * @param codeTemplate - * The codeTemplate from which the JavaScript Doc headers should be extracted - * @param functionName - * The function for which the description headers should be extracted. If none is provided, the first JavaScript Doc header will be - * used (this is e.g. the case if the code template does not contain a function) (OPTIONAL) - * @return A JSON object containing the following attributes: - *
    - *
  • description - the description of the function or code template (OPTIONAL)
  • - *
  • parameters - the name and description of the function parameters (OPTIONAL)
  • - *
  • returnValue - the description of the return value of the function (OPTIONAL)
  • - *
- * The return value is null if the required description was not found - */ - private JSONObject getCodeTemplateDescription(JSONObject codeTemplate, String functionName) { - JSONObject properties = null; - String code, javascriptDocHeader, parameters, parameterName, parameterDescription, parameterList; - Matcher headerMatcher, codeTemplateHeaderDescriptionMatcher, codeTemplateHeaderParameterMatcher, codeTemplateHeaderReturnValueMatcher, - functionParameterMatcher; - - // depending on the mirth version - if (!codeTemplate.has("code")) { - // the code section is encapsulated into properties. Thus make sure the right spot is accessed - codeTemplate = codeTemplate.getJSONObject("properties"); - } - // obtain the code block (which contains the header(s)) - code = codeTemplate.getString("code"); - // determine if the description for a specific function is wanted - boolean isFunction = (functionName != null); - - // determine the right matcher depending on if the code template contains a function - headerMatcher = isFunction ? functionHeaderPattern.matcher(code) : codeTemplateHeaderPattern.matcher(code); - // now find the correct header - while (headerMatcher.find()) { - // make sure that the right function documentation is read - if (isFunction && !functionName.equals(headerMatcher.group(2) + "()")) { - continue; - } - - properties = new JSONObject(); - // now analyze the JavaScript Doc header - javascriptDocHeader = headerMatcher.group(1); - - if (javascriptDocHeader == null) { - javascriptDocHeader = ""; - } - // extract the function description - codeTemplateHeaderDescriptionMatcher = codeTemplateHeaderDescriptionPattern.matcher(javascriptDocHeader); - if (codeTemplateHeaderDescriptionMatcher.find()) { - // and add it to the record - properties.put("description", codeTemplateHeaderDescriptionMatcher.group(1).replaceAll("\\s*(\\r?\\n|\\r)", "\n") - .replaceAll("(?i)(\\r?\\n|\\r)", "\n").replaceAll("(?i)(?)(\\n)", "
\n")); - } - - // if the code template contains no functions - if (!isFunction) { - // analysis ends here - return properties; - } - - // extract the parameters - parameters = ""; - parameterList = headerMatcher.group(3); - // stores the detected description - HashMap detectedDescription = new HashMap(); - - codeTemplateHeaderParameterMatcher = codeTemplateHeaderParameterPattern.matcher(javascriptDocHeader); - // detect all parameter descriptions in the function header - while (codeTemplateHeaderParameterMatcher.find()) { - // extract the parameter name - parameterName = codeTemplateHeaderParameterMatcher.group(1).trim(); - // the parameter description - parameterDescription = codeTemplateHeaderParameterMatcher.group(2).trim(); - // remove a potential "-" prefix - if (parameterDescription.charAt(0) == '-') { - parameterDescription = parameterDescription.substring(1); - } - // adjust description - parameterDescription = parameterDescription.replaceAll("\\s*(\\r?\\n|\\r)", "\n").replaceAll("(?i)(\\r?\\n|\\r)", "\n") - .replaceAll("(?i)(?)(\\n)", "
\n"); - // and add it to the list of detected descriptions - detectedDescription.put(parameterName, parameterDescription); - } - - // generate documentation for all function parameters - functionParameterMatcher = functionParameterPattern.matcher(parameterList); - while (functionParameterMatcher.find()) { - // extract the parameter name - String name = functionParameterMatcher.group(); - // the parameter description - String description = ""; - // if there is a description for this parameter in the function header - if (detectedDescription.containsKey(name)) { - // load it from the list of detected descriptions - description = detectedDescription.get(name); - } - // and add the formatted parameter - parameters += String.format("%s\t%s\n", name, description); - } - if (!parameters.isEmpty()) { - parameters = "" + parameters + "
"; - // add the formatted parameter list to the result set - properties.put("parameters", parameters); - } - - // finally extract the return value description - codeTemplateHeaderReturnValueMatcher = codeTemplateHeaderReturnValuePattern.matcher(javascriptDocHeader); - if (codeTemplateHeaderReturnValueMatcher.find()) { - // add the return value description - properties.put("returnValue", codeTemplateHeaderReturnValueMatcher.group(1).replaceAll("\\s*(\\r?\\n|\\r)", "\n") - .replaceAll("(?i)(\\r?\\n|\\r)", "\n").replaceAll("(?i)(?)(\\n)", "
\n")); - } - - // header was found - not need for further investigation - break; - } - - return properties; - } - - /** - * Provides the name of a code template that corresponds to a given id - * - * @param codeTemplateId - * The id of the code template for which the name should be obtained - * @return name of the code template or null if no code template could be found that corresponds to the given id - * @throws ServiceUnavailableException - */ - private String getCodeTemplateNameById(String codeTemplateId) throws ServiceUnavailableException { - return getCodeTemplateNameById().get(codeTemplateId); - } - - /** - * Provides a map that allows to obtain a code template name by it's id - * - * @return The mapping - * @throws ServiceUnavailableException - */ - private HashMap getCodeTemplateNameById() throws ServiceUnavailableException { - // if the mapping was not yet created - if (this.codeTemplateNameById == null) { - // make sure it exists before returning the reference - getCodeTemplateInfo(); - } - - return this.codeTemplateNameById; - } - - /** - * Provides the ID of a code template that corresponds to a given name - * - * @param codeTemplateName - * The name of the code template for which the ID should be obtained - * @return the ID of the code template or null if no code template could be found that corresponds to the given name - * @throws ServiceUnavailableException - */ - private String getCodeTemplateIdByName(String codeTemplateId) throws ServiceUnavailableException { - return getCodeTemplateIdByName().get(codeTemplateId); - } - - /** - * Provides a map that allows to obtain a code template ID by it's name - * - * @return The mapping - * @throws ServiceUnavailableException - */ - private HashMap getCodeTemplateIdByName() throws ServiceUnavailableException { - // if the mapping was not yet created - if (this.codeTemplateIdbyName == null) { - // make sure it exists before returning the reference - getCodeTemplateInfo(); - } - - return this.codeTemplateIdbyName; - } - - /** - * Provides the id of a channel that corresponds to a given name - * - * @param channelName - * The name of the channel for which the id should be obtained - * @return id of the channel or null if no channel could be found that corresponds to the given name - * @throws ServiceUnavailableException - * @throws ConfigurationException - */ - private String getChannelNameById(String channelId) throws ServiceUnavailableException, ConfigurationException { - - return getChannelNameById().get(channelId); - } - - /** - * Maps channel ids to their name - * - * @return The mappings - * @throws ServiceUnavailableException - * @throws ConfigurationException - */ - private HashMap getChannelNameById() throws ServiceUnavailableException, ConfigurationException { - // if the mapping was not yet created - if (this.channelNameById == null) { - // make sure it exists before returning the reference - getChannelInfo(); - } - - return this.channelNameById; - } - - /** - * Provides the id of a channel that corresponds to a given name - * - * @param channelName - * The name of the channel for which the id should be obtained - * @return id of the channel or null if no channel could be found that corresponds to the given name - * @throws ServiceUnavailableException - * @throws ConfigurationException - */ - private String getChannelIdByName(String channelName) throws ServiceUnavailableException, ConfigurationException { - - return getChannelIdByName().get(channelName); - } - - - /** - * Maps channel names to their channel id - * - * @return The map containing the mappings - * @throws ServiceUnavailableException - * @throws ConfigurationException - */ - private HashMap getChannelIdByName() throws ServiceUnavailableException, ConfigurationException { - // if the mapping was not yet created - if (this.channelIdbyName == null) { - // make sure it exists before returning the reference - getChannelInfo(); - } - - return this.channelIdbyName; - } - - /** - * Detects the functions that are used by the code templates by parsing the code template source code. It caches a mapping between the referencing - * and referenced functions as well as the other way round. - * - * @param xml - * The source code of the code templates (code template definition) - */ - private void buildUpTemplateToTemplateRelationships(String xml) { - - String codeTemplateDefinition = null; - int functionNameBegin, functionNameEnd; - Matcher codeTemplateMatcher, nameMatcher, functionReferenceMatcher; - - this.functionLinkedByFunctions = new HashMap>(); - this.functionUsesFunctions = new HashMap>(); - - codeTemplateMatcher = codeTemplatePattern.matcher(xml); - while (codeTemplateMatcher.find()) { - // extract the code template definition - codeTemplateDefinition = codeTemplateMatcher.group(); - // if no function name will be found, the code template does not contain a function but might anyway have code that references a custom - // function. - // thus, in this case, use the code template name, instead - TODO: Maybe this should be replaced by code template id as name is not - // guaranteed to be unique - String currentFunctionName = null; - - int functionStart, functionEnd = -1; - String functionBody = null; - TreeSet detectedFunctions = null; - - // now try to extract the function name - Matcher functionNameMatcher = functionNamePattern.matcher(codeTemplateDefinition); - if (functionNameMatcher.find()) { - // there is a function definition in this code template - functionStart = functionNameMatcher.end(); - // extract the first function name - currentFunctionName = functionNameMatcher.group(1) + "()"; - } else { - functionStart = codeTemplateDefinition.indexOf(""); - // this code template does not contain any function definitions but might contain references. - nameMatcher = namePattern.matcher(codeTemplateDefinition); - // Thus use template name as name - currentFunctionName = nameMatcher.find() ? nameMatcher.group(1) : "Unknown_" + UUID.randomUUID(); - } - - // scan all functions within a code template (as JavaScript is no regular language, pure regex can't be used for function body extraction) - while (functionStart != -1) { - // get end index of first function definition - which is the whole code if no further function is found - functionEnd = functionNameMatcher.find() ? functionNameMatcher.end() : codeTemplateDefinition.length(); - - // extract the function definition - functionBody = codeTemplateDefinition.substring(functionStart, functionEnd); - // prepare the function body for function detection - functionBody = prepareForFunctionParsing(functionBody); - // create a container for the detected functions - detectedFunctions = new TreeSet(); - - // needed to avoid detecting regular expressions as false positives - Matcher roughRegexMatcher = roughRegexDetectionPattern.matcher(functionBody); - - // now scan for functions - functionReferenceMatcher = functionReferenceDetectionPattern.matcher(functionBody); - - // and add all detected functions to a distinct list - while (functionReferenceMatcher.find()) { - // get the name of the referenced function - String referencedFunctionName = functionReferenceMatcher.group(1); - // if the current detected function is part of the filter list - if (functionFilter.contains(referencedFunctionName)) { - // it's a false positive - omit it - continue; - } - - // if this reference was already detected - if(detectedFunctions.contains(referencedFunctionName)) { - // no need to scan it again - continue; - } - - // determine the location of the function reference w/i the code - functionNameBegin = functionReferenceMatcher.start(1); - functionNameEnd = functionReferenceMatcher.end(1); - - boolean isRegex = false; - // start the regex search over from the begin of the source code - roughRegexMatcher.reset(); - - // check all regular expressions - while (roughRegexMatcher.find()) { - // Check if this function call is inside a regular expression - if (functionNameBegin > roughRegexMatcher.start() && functionNameEnd < roughRegexMatcher.end()) { - // Oops this was a regex - isRegex = true; - break; - } - // if the regex already starts after the function name - if (functionNameEnd < roughRegexMatcher.start()) { - // no further false positive scanning for this function is needed - break; - } - } - - // if function reference was detected as regex - if(isRegex) { - // ignore it - continue; - } - - // it's no function w/o brackets ;-) - referencedFunctionName += "()"; - // add the function to the result set (add this function/code template to the list of functions that use the currently detected function) - detectedFunctions.add(referencedFunctionName); - // if the detected function was not yet referenced - if (!this.functionLinkedByFunctions.containsKey(referencedFunctionName)) { - // create a new entry for it - this.functionLinkedByFunctions.put(referencedFunctionName, new TreeSet()); - } - // add the current function/code template to the list of functions that reference the function for which the reference was - // detected - this.functionLinkedByFunctions.get(referencedFunctionName).add(currentFunctionName); - } - - if (detectedFunctions.size() > 0) { - // add an entry for the function - this.functionUsesFunctions.put(currentFunctionName, new ArrayList(detectedFunctions)); - } - - // get start index of next function definition - functionStart = functionNameMatcher.find() ? functionNameMatcher.end() : -1; - } - } - } - - /** - * Provides a map containing metadata of all channels. If it does not exist, it will be generated. - * - * @return A HashMap identifying each channel by it's id and providing the following information per channel: - *
    - *
  • Display name - The name of the channel
  • - *
  • Id - The UUID of the channel
  • - *
  • Description - A description of the purpose of this channel
  • - *
  • Last modified - Timestamp of the last modification
  • - *
  • Display date - A human readable representation of the Last modified timestamp in the format dd.MM.yyyy, - * HH:mm:ss
  • - *
  • Version - The version of the channel. Every change increments the version number by 1.
  • - *
  • Type - channel
  • - *
  • Is disabled - true, if the channel is disabled,not set if channel is enabled (OPTIONAL)
  • - *
- * @throws ConfigurationException - * @throws ServiceUnavailableException - */ - private HashMap getChannelInfo() throws ConfigurationException, ServiceUnavailableException { - - // lazy fetching - if (this.channelInfo == null) { - // initialize container - HashMap channelInfo = new HashMap(); - this.channelFunctionReferences = new HashMap>(); - this.channelReferencesToFunction = new HashMap>(); - this.channelInternalFunctionsByChannelId = new HashMap>(); - this.channelIdbyName = new HashMap(); - this.channelNameById = new HashMap(); - - // get info about all channels. The pure xml is used for finding channel/code template relationships - String xml = getResponseAsXml(connectToRestService("/api/channels")); - // scan the channel code for code template usage - buildUpCodeTemplateRelationships(xml); - // and prepare it for metadata parsing - JSONObject raw = XML.toJSONObject(xml); - try { - raw = raw.getJSONObject("list"); - } catch (JSONException e) { - // if the channel group is empty, there will be no channel information - this.channelInfo = channelInfo; - return this.channelInfo; - } - - // try to load caches - HashMap channelLastModified = getChannelLastModified(false); - HashMap channeState = getChannelState(false); - - // assure that an array will be used - JSONArray channels = (raw.get("channel") instanceof JSONArray) ? raw.getJSONArray("channel") : (new JSONArray()).put(raw.get("channel")); - - // and extract the relevant information of each channel - for (Object element : channels) { - // get next channel - JSONObject channel = (JSONObject) element; - // create a new element - JSONObject metaData = new JSONObject(); - // add the display name of the channel - String channelName = channel.getString("name"); - metaData.accumulate("Display name", channelName); - // add the id of the channel - String channelId = channel.getString("id"); - metaData.accumulate("Id", channelId); - // add the channel to the name to id mapping - getChannelIdByName().put(channelName, channelId); - // add the channel to the id to name mapping - getChannelNameById().put(channelId, channelName); - // add the version of the channel - metaData.accumulate("Version", channel.get("revision")); - // not yet sure for what the item type is needed - metaData.accumulate("Type", CHANNEL); - - // add the last modified date for sorting (structure changed w/ Mirth version 3.6.0) - Long lastModified = null; - // and also the indicator if the channel is enabled - boolean channelDisabled = false; - - // Now try to determine the last modified date of the channel - if (channelLastModified.containsKey(channelId)) { - // if the latest api version is supported, it can be taken from the cache - lastModified = channelLastModified.get(channelId); - // as well as the channel state - channelDisabled = !channeState.get(channelId); - } else if (channel.has("lastModified")) { - // if channel uses the old format, the information is provided directly in the channel structure (to where it belongs from my - // point of view) - lastModified = channel.getJSONObject("lastModified").getLong("time"); - channelDisabled = !channel.getBoolean("enabled"); - } else { - // as a last resort try location of newer format (structure changed w/ Mirth version 3.6.0) - try { - // unfortunately, the query method does not work like advertised. Thus try/catch is needed - JSONObject infoLocation = (JSONObject) channel.query("/exportData/metadata"); - // let's do it brute force as it is caught anyway - lastModified = infoLocation.getJSONObject("lastModified").getLong("time"); - channelDisabled = !infoLocation.getBoolean("enabled"); - } catch (Exception e) { - // root.error("Last Modified - Exception!"); - } - } - // add the last modified date in ms - metaData.accumulate("Last modified", lastModified); - // add the last modified date for displaying - metaData.accumulate("Display date", (lastModified != null) ? formatDate(lastModified) : "-"); - - String description = channel.getString("description"); - // extract all documented channel changes and sort it from newest to oldest - TreeMap changes = new TreeMap(Collections.reverseOrder()); - Matcher changesMatcher = changesPattern.matcher(description); - while (changesMatcher.find()) { - String changeDate, changeDescription; - Date parsedDate = null; - try { - parsedDate = changeParseDateFormat.parse(changesMatcher.group(1)); - // format the data of the current change - changeDate = changeDisplayDateFormat.format(parsedDate); - - } catch (java.text.ParseException e) { - // if the date string format is invalid, keep it in the initial format - changeDate = changesMatcher.group(1); - // there is no date for ordering. Thus use now - parsedDate = new Date(System.currentTimeMillis()); - // but log an error - logger.error("The change date \"" + (changesMatcher.group(1) + "\" has an invalid format. It must be in the format yyyyDDmm!")); - } - // and also extract the change description - changeDescription = changesMatcher.group(2).trim(); - changes.put(parsedDate.getTime(), "" + changeDate + "\t" + changeDescription + ""); - } - - // if changes were found - if (changes.size() > 0) { - // add changes as an attribute - metaData.accumulate("Changes", - "" + changes.values().stream().collect(Collectors.joining("\n")) + "
"); - // and remove the change entries from the description - description = changesMatcher.replaceAll(""); - // and also a version heading - description = description.replaceFirst("(?i)[\\s\\#]*versions?\\s*\\:?[\\s\\#]*(?:\\r?:\\n|\\n)", ""); - } - - // now check for inbound and outbound interfaces to external systems - TreeMap inboundInterfaces = new TreeMap(); - TreeMap outboundInterfaces = new TreeMap(); - - Matcher systemInterfaceMatcher = systemInterfacePattern.matcher(description); - while (systemInterfaceMatcher.find()) { - String direction, externalSystem, dataType, connector; - // INbound or OUTbound - direction = systemInterfaceMatcher.group(1); - // the connector id of the mirth channel - connector = systemInterfaceMatcher.group(2); - // the transferred data type - dataType = systemInterfaceMatcher.group(3).replaceFirst("\\_.+$", ""); - // The name of the external system - externalSystem = systemInterfaceMatcher.group(4); - - // add the entry to the respective map ordered by external system name - if (direction.equalsIgnoreCase("IN")) { - inboundInterfaces.put(externalSystem, - String.format("
  • %s from %s (connector %s)
  • ", dataType, externalSystem, connector)); - } else { - outboundInterfaces.put(externalSystem, - String.format("
  • %s to %s (connector %s)
  • ", dataType, externalSystem, connector)); - } - } - - // if external system interfaces were found - if ((inboundInterfaces.size() > 0) || (outboundInterfaces.size() > 0)) { - // remove the interface description from the channel description - description = systemInterfaceMatcher.replaceAll(""); - - // if there were any inbound interfaces detected (data coming from an application interface to the mirth channel) - if (inboundInterfaces.size() > 0) { - // add an inbound property - metaData.accumulate("Inbound Interfaces", - "
      \n" + inboundInterfaces.values().stream().collect(Collectors.joining("\n")) + "
    \n"); - } - - // if there were any outbound interfaces detected (data going to an application interface from the mirth channel) - if (outboundInterfaces.size() > 0) { - // add an outbound property - metaData.accumulate("Outbound Interfaces", - "
      \n" + outboundInterfaces.values().stream().collect(Collectors.joining("\n")) + "
    \n"); - } - } - - // add the description of the channel - metaData.accumulate("Description", - description.replaceFirst("^[\\p{Cntrl}\\s]*", "").replaceFirst("[\\p{Cntrl}\\s]*$", "").replaceAll("\\r\\n|\\r|\\n", "
    ")); - - if (channelDisabled) { - // indicate if channel is disabled - metaData.accumulate("Is disabled", channelDisabled); - } - - /** the following code accumulates the detected channel issues */ - - // get a validated list of used functions - TreeMap validatedFunctions = validateFunctionReferences(channelId); - - // check for unknown functions - TreeSet unknownFunctions = getUnknownChannelFunctions(channelId); - if (unknownFunctions != null) { - // first check if there is already an issue attribute for this channel - if (!metaData.has("Issues")) { - // if not, create it - metaData.put("Issues", new JSONObject()); - } - // add the list of unknown functions - metaData.getJSONObject("Issues").put("unknownFunctions", unknownFunctions); - } - - // get the validated list of (to be) referenced libraries - JSONObject libraryReferences = generateValidatedReferencedLibraryList(channelId, - (validatedFunctions != null) ? validatedFunctions.keySet() : null); - - // check for missing code template library references - JSONArray missingReferences = libraryReferences.getJSONArray("issues"); - // and if there are any - if (missingReferences.length() > 0) { - // first check if there is already an issue attribute for this channel - if (!metaData.has("Issues")) { - // if not, create it - metaData.put("Issues", new JSONObject()); - } - // add the list of missing libraries - metaData.getJSONObject("Issues").put("missingReferences", missingReferences); - } - - // write the meta data to cache - channelInfo.put(metaData.getString("Id"), metaData); - } - this.channelInfo = channelInfo; - // update the update indicator - this.lastUpdate = System.currentTimeMillis(); - } - - return this.channelInfo; - } - - /** - * Provides information about an external resource that is referenced by Mirth - * @param resourceName The name of the external resource - * @return A JSON object containing the following information: - *
      - *
    • name - The name of the resource
    • - *
    • id - The mirth internal id of the resource
    • - *
    • location - The path to which the resource is pointing
    • - *
    • description - The description text of the resource
    • - *
    • type - The type of the resource
    • - *
    - * @throws ServiceUnavailableException - */ - private JSONObject getExternalResource(String resourceName) throws ServiceUnavailableException { - - if(this.externalResources == null) { - JSONObject resourceInfoRaw = null; - this.externalResources = new HashMap(); - - // try to load the channel meta data from the API - resourceInfoRaw = getResponseAsJson(connectToRestService("/api/server/resources")); - - try { - // make sure the work continues w/ an JSON array - JSONArray resourceInfo = (resourceInfoRaw.get("com.mirth.connect.plugins.directoryresource.DirectoryResourceProperties") instanceof JSONArray) ? resourceInfoRaw.getJSONArray("com.mirth.connect.plugins.directoryresource.DirectoryResourceProperties") - : (new JSONArray()).put(resourceInfoRaw.get("com.mirth.connect.plugins.directoryresource.DirectoryResourceProperties")); - // cache external resources info - for (int index = 0; index < resourceInfo.length(); index++) { - // fetch the relevant resource attributes - String name = resourceInfo.getJSONObject(index).getString("name"); - String id = resourceInfo.getJSONObject(index).getString("id"); - String location = resourceInfo.getJSONObject(index).getString("directory"); - String description = resourceInfo.getJSONObject(index).getString("description"); - String type = resourceInfo.getJSONObject(index).getString("type"); - - // create the object - JSONObject entry = new JSONObject(); - entry.accumulate("name", name); - entry.accumulate("id", id); - entry.accumulate("location", location); - entry.accumulate("description", description); - entry.accumulate("type", type); - - // and add it to the cache - this.externalResources.put(name, entry); - } - } catch (Exception e) { - logger.error("Owh, getExternalResource() has to be revised! \n" + e.getMessage()); - } - } - - // try to fetch the external resource by the given name - return this.externalResources.get(resourceName); - } - - /** - * Provides a cache of all last modified dates of channels by using the new Mirth API version - * - * @param forceReload - * A flag that causes, when set to true, the cache to reload regardless of its pre-existence - * @return The last modified cache for all channels or an empty cache if the Mirth server does not support the metadata web-service or is not - * reachable - */ - private HashMap getChannelLastModified(boolean forceReload) { - if (forceReload || (this.channelLastModified == null)) { - cacheChannelMetaData(); - } - - return this.channelLastModified; - } - - /** - * Provides a cache of all channel states by using the new Mirth API version - * - * @param forceReload - * A flag that causes, when set to true, the cache to reload regardless of its pre-existence - * @return The state cache for all channels or an empty cache if the Mirth server does not support the metadata web-service or is not reachable - */ - private HashMap getChannelState(boolean forceReload) { - if (forceReload || (this.channelState == null)) { - cacheChannelMetaData(); - } - - return this.channelState; - } - - /** - * Caches the last modified date as well as the state of channels by using the new Mirth API (channelMetadata) - */ - private void cacheChannelMetaData() { - - // initialize the caches - this.channelState = new HashMap(); - this.channelLastModified = new HashMap(); - - JSONObject channelMetaData = null; - try { - // try to load the channel meta data from the API - channelMetaData = getResponseAsJson(connectToRestService("/api/server/channelMetadata")); - channelMetaData = channelMetaData.getJSONObject("map"); - } catch (Exception e) { - // this may fail as it is not supported by older Mirth versions - return; - } - try { - // make sure the work continues w/ an JSON array - JSONArray channel = (channelMetaData.get("entry") instanceof JSONArray) ? channelMetaData.getJSONArray("entry") - : (new JSONArray()).put(channelMetaData.get("entry")); - - // add all last modified time stamps and channel states to the caches - for (int index = 0; index < channel.length(); index++) { - // get the id of the current channel - String channelId = channel.getJSONObject(index).getString("string"); - // access the actual metadata section of the channel - channelMetaData = channel.getJSONObject(index).getJSONObject("com.mirth.connect.model.ChannelMetadata"); - - // there are corrupt configurations where the attribute is actually missing - no idea why... - if (channelMetaData.has("lastModified")) { - // add the last modified date for the channel to the cache - this.channelLastModified.put(channelId, channelMetaData.getJSONObject("lastModified").getLong("time")); - } - - // and add the channel state to the cache. Better check the attribute first to avoid potential issues like for lastModified - this.channelState.put(channelId, channelMetaData.has("enabled") ? channelMetaData.getBoolean("enabled") : true); - } - } catch (Exception e) { - logger.error("Owh, getChannelLastModified() has to be revised! \n" + e.getMessage()); - } - } - - /** - * Detects the functions that are used by the channels by parsing the channel source code. It caches a mapping between channel and functions as - * well as function and channels - * - * @param xml - * The source code of the channels (channel definition) - * @throws ConfigurationException - */ - private synchronized void buildUpCodeTemplateRelationships(String xml) throws ConfigurationException { - - String channelDefinition = null; - Matcher channelMatcher, idMatcher, functionReferenceMatcher, functionNameMatcher; - TreeSet detectedFunctions = null; - String channelId = null; - - channelMatcher = channelPattern.matcher(xml); - // loop over all channels - while (channelMatcher.find()) { - // get the channel definition - channelDefinition = channelMatcher.group(); - - // detect the channel id - idMatcher = idPattern.matcher(channelDefinition); - if (!idMatcher.find()) { - // should never happen - throw new ConfigurationException("Unable to find channel id: \n" + channelDefinition); - } - // extract the channel id from the xml - channelId = idMatcher.group(1); - - // prepare channel definition for function detection - channelDefinition = prepareForFunctionParsing(channelDefinition); - - // needed to avoid detecting regular expressions as false positives - Matcher roughRegexMatcher = roughRegexDetectionPattern.matcher(channelDefinition); - - // create a container for the detected functions - detectedFunctions = new TreeSet(); - - // now scan for functions - functionReferenceMatcher = functionReferenceDetectionPattern.matcher(channelDefinition); - - int functionNameBegin = 0; - int functionNameEnd = 0; - // and add all detected functions to a distinct list - while (functionReferenceMatcher.find()) { - - // get the next detected function - String functionName = functionReferenceMatcher.group(1); - // if the current detected function is part of the filter list - if (functionFilter.contains(functionName)) { - // omit it as it is a false positive (e.g. an SQL function) - continue; - } - - // if this reference was already detected - if(detectedFunctions.contains(functionName)) { - // no need to scan it again - continue; - } - - // determine the location of the function reference w/i the code - functionNameBegin = functionReferenceMatcher.start(1); - functionNameEnd = functionReferenceMatcher.end(1); - - boolean isRegex = false; - // start the regex search over from the begin of the source code - roughRegexMatcher.reset(); - - // check all regular expressions - while (roughRegexMatcher.find()) { - // Check if this function call is inside a regular expression - if (functionNameBegin > roughRegexMatcher.start() && functionNameEnd < roughRegexMatcher.end()) { - // Oops this was a regex - isRegex = true; - break; - } - // if the regex already starts after the function name - if (functionNameEnd < roughRegexMatcher.start()) { - // no further false positive scanning for this function is needed - break; - } - } - - // if function reference was detected as regex - if(isRegex) { - // ignore it - continue; - } - - // make it sexy - functionName += "()"; - // add the function to the result set - detectedFunctions.add(functionName); - // as there seem to be concurrency situations where the container is removed during it's filling - if(this.channelReferencesToFunction == null) { - // add this for security reasons - this.channelReferencesToFunction = new HashMap>(); - logger.warn("There seems to be a concurrency issue. Container channelReferencesToFunction was deleted when it was about to be filled."); - } - // if there is not yet a container for this function - if (!this.channelReferencesToFunction.containsKey(functionName)) { - // create one - that way a bidirectional mapping is possible (allows to show which channel is using this function) - this.channelReferencesToFunction.put(functionName, new TreeSet()); - } - // and add the reference to this channel to the list of channels using this function - this.channelReferencesToFunction.get(functionName).add(channelId); - } - - // if referenced functions where detected - if (detectedFunctions.size() > 0) { - // an entry for the channel - this.channelFunctionReferences.put(channelId, new ArrayList(detectedFunctions)); - } - - // function calls have been handled. Now check if there are function definitions in the channel itself - functionNameMatcher = functionNamePattern.matcher(channelDefinition); - ArrayList channelFunctions = new ArrayList(); - // scan channel definition for internal functions - while (functionNameMatcher.find()) { - // add the function to the list - channelFunctions.add(functionNameMatcher.group(1) + "()"); - } - // if internal functions where found - if (channelFunctions.size() > 0) { - // add them to cache - this.channelInternalFunctionsByChannelId.put(channelId, channelFunctions); - } - } - } - - /** - * Cleans the code to prepare it for function detection. The function aims to remove the following elements from the code in order to avoid false - * positives at function parsing: - *
      - *
    • Code comments
    • - *
    • Object instantiations (new Xyz())
    • - *
    • Regex patterns
    • - *
    • Java Strings (text in quotes)
    • - *
    • JavaScript Strings (text in apostrophes)
    • - *
    • Description tags
    • - *
    • Select tags
    • - *
    - * - * @param code - * The code that should be prepared for parsing - * @return The cleaned up code - */ - private String prepareForFunctionParsing(String code) { - String result = null; - - if ((code != null) && (code.length() > 0)) { - // remove all base64 blocks (it will otherwise bust the stack) - result = base64DetectionPattern.matcher(code).replaceAll("$1"); - // remove all comments - result = commentDetectionPattern.matcher(code).replaceAll(""); - // and also remove all object instantiations (like "new String()") - result = instantationDetectionPattern.matcher(result).replaceAll(""); - // remove all java strings - // IMPORTANT: This must be done before filtering JavaScript strings as apostrophes are also used in text - result = javaStringDetectionPattern.matcher(result).replaceAll(""); - // remove all JavaScript strings - result = javascriptStringDetectionPattern.matcher(result).replaceAll(""); - // remove all "" tags - result = descriptionTagDetectionPattern.matcher(result).replaceAll(""); - // remove all "" tags - result = nameTagDetectionPattern.matcher(result).replaceAll(""); - // remove all "" tags - result = subjectTagDetectionPattern.matcher(result).replaceAll(""); - // remove all CDATA query definitions - result = cdataDetectionPattern.matcher(result).replaceAll(""); - // remove all sql queries that are defined in query tags - result = queryDetectionPattern.matcher(result).replaceAll(""); - // remove all sql queries that are defined in empty tags - result = emptyTagDetectionPattern.matcher(result).replaceAll(""); - // and finally remove all "