diff --git a/custom-extensions/build.xml b/custom-extensions/build.xml
new file mode 100644
index 000000000..8b1f1c64c
--- /dev/null
+++ b/custom-extensions/build.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/.classpath b/custom-extensions/dynamic-lookup-gateway/.classpath
new file mode 100644
index 000000000..4bcc1b748
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/.classpath
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/.gitignore b/custom-extensions/dynamic-lookup-gateway/.gitignore
new file mode 100644
index 000000000..10a96a82f
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/.gitignore
@@ -0,0 +1,2 @@
+# Build output
+/build/
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/.project b/custom-extensions/dynamic-lookup-gateway/.project
new file mode 100644
index 000000000..c2b7a38a3
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/.project
@@ -0,0 +1,15 @@
+
+
+ dynamic-lookup-gateway
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/build.properties b/custom-extensions/dynamic-lookup-gateway/build.properties
new file mode 100644
index 000000000..299f27a53
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/build.properties
@@ -0,0 +1,26 @@
+# Plugin name
+plugin.name=dynamic-lookup-gateway
+
+# Build output
+build.dir=build
+classes.dir=${build.dir}/classes
+dist.dir=${build.dir}/dist
+plugin.lib=libs
+
+# Mirth dependency paths (relative to plugin location)
+server.lib=../../server/lib
+client.lib=../../client/lib
+
+# Paths to compiled class files of Mirth modules (used for referencing in classpath)
+server.classes.dir=../../server/classes
+client.classes.dir=../../client/classes
+
+# Resource directories (Maven-style)
+server.resources.dir=server/src/main/resources
+
+# Path to MC setup directory where final packaged extensions go
+server.setup.extensions=../../server/setup/extensions
+server.setup.conf=../../server/setup/conf
+# Signing properties
+keystore_property_file=keystore.properties
+signjar_thread_count=4
diff --git a/custom-extensions/dynamic-lookup-gateway/build.xml b/custom-extensions/dynamic-lookup-gateway/build.xml
new file mode 100644
index 000000000..2e87994c3
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/build.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/dialog/LookupGroupDialog.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/dialog/LookupGroupDialog.java
new file mode 100644
index 000000000..b63c2f48c
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/dialog/LookupGroupDialog.java
@@ -0,0 +1,253 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.dialog;
+
+import com.mirth.connect.client.ui.Frame;
+import com.mirth.connect.client.ui.MirthDialog;
+import com.mirth.connect.client.ui.PlatformUI;
+import com.mirth.connect.client.ui.UIConstants;
+import com.mirth.connect.client.ui.components.MirthFieldConstraints;
+
+import com.mirth.connect.plugins.dynamiclookup.client.exception.LookupApiClientException;
+import com.mirth.connect.plugins.dynamiclookup.client.service.LookupServiceClient;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+
+import net.miginfocom.swing.MigLayout;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.swing.JPanel;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JComboBox;
+import javax.swing.WindowConstants;
+import javax.swing.JScrollPane;
+import javax.swing.BorderFactory;
+import javax.swing.JSeparator;
+
+import java.awt.Dimension;
+
+public class LookupGroupDialog extends MirthDialog {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ private JLabel nameLabel;
+ private JTextField nameField;
+
+ private JLabel descriptionLabel;
+ private JTextArea descriptionField;
+ private JScrollPane descriptionScrollPane;
+
+ private JLabel versionLabel;
+ private JTextField versionField;
+
+ private JLabel cacheSizeLabel;
+ private JTextField cacheSizeField;
+
+ private JLabel cachePolicyLabel;
+ private JComboBox cachePolicyComboBox;
+
+ private JButton saveButton;
+ private JButton cancelButton;
+
+ private Frame parent;
+ private LookupGroup lookupGroup;
+ private boolean saved = false;
+
+ public LookupGroupDialog(Frame parent, LookupGroup lookupGroup, boolean isEdit) {
+ super(parent, true);
+
+ this.parent = parent;
+ this.lookupGroup = lookupGroup;
+
+ initComponents();
+ initLayout();
+
+ resetComponents(lookupGroup);
+
+ setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+ setTitle(String.format("%s", isEdit ? "Edit Group" : "Add Group"));
+ pack();
+ setLocationRelativeTo(parent);
+ setVisible(true);
+ }
+
+ private void initComponents() {
+ setBackground(UIConstants.BACKGROUND_COLOR);
+ getContentPane().setBackground(getBackground());
+
+ nameLabel = new JLabel("Name:");
+ nameField = new JTextField();
+
+ descriptionLabel = new JLabel("Description:");
+ descriptionField = new JTextArea();
+ descriptionField.setWrapStyleWord(true);
+ descriptionField.setLineWrap(true);
+ descriptionScrollPane = new JScrollPane(descriptionField, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+ descriptionScrollPane.setPreferredSize(new Dimension(200, 50));
+
+ versionLabel = new JLabel("Version:");
+ versionField = new JTextField();
+
+ cacheSizeLabel = new JLabel("Cache Size:");
+ cacheSizeField = new JTextField();
+ cacheSizeField.setDocument(new MirthFieldConstraints(8, false, false, true));
+
+ cachePolicyLabel = new JLabel("Cache Policy:");
+ cachePolicyComboBox = new JComboBox();
+ cachePolicyComboBox.addItem("LRU");
+ cachePolicyComboBox.addItem("FIFO");
+ cachePolicyComboBox.getModel().setSelectedItem("LRU");
+
+ saveButton = new JButton("Save");
+ saveButton.addActionListener(evt -> save());
+
+ cancelButton = new JButton("Cancel");
+ cancelButton.addActionListener(evt -> close());
+ }
+
+ private void initLayout() {
+ setLayout(new MigLayout("insets 12, novisualpadding, hidemode 3, fill"));
+
+ JPanel addPanel = new JPanel(new MigLayout("novisualpadding, hidemode 0, align center, insets 0 0 0 0, fill", "25[right][fill]"));
+
+ addPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ addPanel.setBorder(BorderFactory.createEmptyBorder());
+ addPanel.setMinimumSize(getMinimumSize());
+ addPanel.setMaximumSize(getMaximumSize());
+
+ addPanel.add(nameLabel, "right");
+ addPanel.add(nameField, "w 200!");
+
+ addPanel.add(descriptionLabel, "newline, right");
+ addPanel.add(descriptionScrollPane);
+
+ addPanel.add(versionLabel, "newline, right");
+ addPanel.add(versionField, "w 100!");
+
+ addPanel.add(cacheSizeLabel, "newline, right");
+ addPanel.add(cacheSizeField, "w 100!");
+
+ addPanel.add(cachePolicyLabel, "newline, right");
+ addPanel.add(cachePolicyComboBox, "w 100!");
+
+ add(addPanel, "growx");
+ add(new JSeparator(), "newline, sx, growx");
+
+ add(saveButton, "newline, sx, right, split 2");
+ add(cancelButton);
+ }
+
+ private void resetComponents(LookupGroup lookupGroup) {
+ nameField.setText(lookupGroup.getName());
+ descriptionField.setText(lookupGroup.getDescription());
+ versionField.setText(lookupGroup.getVersion());
+ cacheSizeField.setText(String.valueOf(lookupGroup.getCacheSize()));
+ cachePolicyComboBox.getModel().setSelectedItem(lookupGroup.getCachePolicy());
+ }
+
+ private void save() {
+ if (!validateProperties()) {
+ return;
+ }
+
+ this.lookupGroup.setName(nameField.getText().trim());
+ this.lookupGroup.setDescription(descriptionField.getText().trim());
+ this.lookupGroup.setVersion(versionField.getText().trim());
+ this.lookupGroup.setCacheSize(Integer.parseInt(cacheSizeField.getText().trim()));
+ this.lookupGroup.setCachePolicy((String) cachePolicyComboBox.getSelectedItem());
+
+ try {
+ LookupGroup response;
+ if (this.lookupGroup.getId() > 0) {
+ response = LookupServiceClient.getInstance().updateGroup(this.lookupGroup);
+ } else {
+ response = LookupServiceClient.getInstance().createGroup(this.lookupGroup);
+ }
+
+ this.lookupGroup.setId(response.getId());
+ this.lookupGroup.setCreatedDate(response.getCreatedDate());
+ this.lookupGroup.setUpdatedDate(response.getUpdatedDate());
+
+ this.saved = true;
+
+ close();
+
+ } catch (LookupApiClientException e) {
+ showError(e.getError().getMessage());
+ } catch (Exception e) {
+ logger.error("Unexpected error while saving group", e);
+ showError("Unexpected error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
+ }
+ }
+
+ private void close() {
+ dispose();
+ }
+
+ public boolean isSaved() {
+ return saved;
+ }
+
+ private boolean validateProperties() {
+ boolean valid = true;
+ StringBuilder errorMessage = new StringBuilder();
+
+ // Reset backgrounds
+ resetInvalidComponents();
+
+ String name = nameField.getText().trim();
+ if (StringUtils.isEmpty(name)) {
+ valid = false;
+ nameField.setBackground(UIConstants.INVALID_COLOR);
+ errorMessage.append("Please provide a name.")
+ .append(System.lineSeparator());
+ }
+
+ String version = versionField.getText().trim();
+ if (StringUtils.isEmpty(version)) {
+ valid = false;
+ versionField.setBackground(UIConstants.INVALID_COLOR);
+ errorMessage.append("Please provide a version.")
+ .append(System.lineSeparator());
+ }
+
+ String cacheSize = cacheSizeField.getText().trim();
+ if (StringUtils.isEmpty(cacheSize)) {
+ valid = false;
+ cacheSizeField.setBackground(UIConstants.INVALID_COLOR);
+ errorMessage.append("Please provide a cache size.")
+ .append(System.lineSeparator());
+ }
+
+ if (!valid) {
+ showError(errorMessage.toString());
+ }
+
+ return valid;
+ }
+
+ public void resetInvalidComponents() {
+ nameField.setBackground(null);
+ versionField.setBackground(null);
+ cacheSizeField.setBackground(null);
+ }
+
+ protected void showInformation(String msg) {
+ PlatformUI.MIRTH_FRAME.alertInformation(this, msg);
+ }
+
+ protected void showError(String err) {
+ PlatformUI.MIRTH_FRAME.alertError(this, err);
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/dialog/LookupValueDialog.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/dialog/LookupValueDialog.java
new file mode 100644
index 000000000..590bc445f
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/dialog/LookupValueDialog.java
@@ -0,0 +1,234 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.dialog;
+
+import com.mirth.connect.client.ui.Frame;
+import com.mirth.connect.client.ui.MirthDialog;
+import com.mirth.connect.client.ui.PlatformUI;
+import com.mirth.connect.client.ui.UIConstants;
+
+import com.mirth.connect.plugins.dynamiclookup.client.exception.LookupApiClientException;
+import com.mirth.connect.plugins.dynamiclookup.client.service.LookupServiceClient;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.LookupValueResponse;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+
+import net.miginfocom.swing.MigLayout;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.swing.JLabel;
+import javax.swing.JTextField;
+import javax.swing.JTextArea;
+import javax.swing.JButton;
+import javax.swing.WindowConstants;
+import javax.swing.JPanel;
+import javax.swing.BorderFactory;
+import javax.swing.JSeparator;
+import javax.swing.JOptionPane;
+import javax.swing.JScrollPane;
+
+public class LookupValueDialog extends MirthDialog {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+ private JLabel keyLabel;
+ private JTextField keyField;
+
+ private JLabel valueLabel;
+ private JTextArea valueField;
+ private JScrollPane valueScrollPane;
+ private JButton saveButton;
+ private JButton cancelButton;
+
+ private Frame parent;
+ private LookupValue lookupValue;
+ private LookupGroup lookupGroup;
+ private boolean isEdit = false;
+ private boolean saved = false;
+
+ public LookupValueDialog(Frame parent, LookupValue lookupValue, LookupGroup lookupGroup, boolean isEdit) {
+ super(parent, true);
+
+ this.parent = parent;
+ this.isEdit = isEdit;
+ this.lookupValue = lookupValue;
+ this.lookupGroup = lookupGroup;
+
+ initComponents();
+ initLayout();
+
+ resetComponents(lookupValue);
+
+ setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+ setTitle(String.format("Group %s - %s", lookupGroup.getName(), isEdit ? "Edit Value" : "Add Value"));
+ pack();
+ setLocationRelativeTo(parent);
+ setVisible(true);
+ }
+
+ private void initComponents() {
+ setBackground(UIConstants.BACKGROUND_COLOR);
+ getContentPane().setBackground(getBackground());
+
+ keyLabel = new JLabel("Key:");
+ keyField = new JTextField();
+
+ valueLabel = new JLabel("Value:");
+ valueField = new JTextArea(3, 20);
+ valueField.setLineWrap(true);
+ valueField.setWrapStyleWord(true);
+ valueScrollPane = new JScrollPane(valueField);
+ valueScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+ valueScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+
+ saveButton = new JButton("Save");
+ saveButton.addActionListener(evt -> save());
+
+ cancelButton = new JButton("Cancel");
+ cancelButton.addActionListener(evt -> close());
+ }
+
+ private void initLayout() {
+ setLayout(new MigLayout("insets 12, novisualpadding, hidemode 3, fill"));
+
+ JPanel addPanel = new JPanel(new MigLayout("novisualpadding, hidemode 0, align center, insets 0 0 0 0, fill", "25[right][fill]"));
+
+ addPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ addPanel.setBorder(BorderFactory.createEmptyBorder());
+ addPanel.setMinimumSize(getMinimumSize());
+ addPanel.setMaximumSize(getMaximumSize());
+
+ addPanel.add(keyLabel, "right");
+ addPanel.add(keyField, "w 200!");
+
+ addPanel.add(valueLabel, "newline, right");
+ addPanel.add(valueScrollPane, "w 200!, h 80!");
+
+ add(addPanel, "growx");
+ add(new JSeparator(), "newline, sx, growx");
+
+ add(saveButton, "newline, sx, right, split 2");
+ add(cancelButton);
+ }
+
+ private void resetComponents(LookupValue lookupValue) {
+ keyField.setText(lookupValue.getKeyValue());
+ valueField.setText(lookupValue.getValueData());
+
+ keyField.setEnabled(!this.isEdit);
+ }
+
+ private void save() {
+ if (!validateProperties()) {
+ return;
+ }
+ try {
+ this.lookupValue.setKeyValue(keyField.getText().trim());
+ this.lookupValue.setValueData(valueField.getText().trim());
+
+ if (!this.isEdit) {
+ boolean exists;
+ try {
+ exists = LookupServiceClient.getInstance().checkValueExists(this.lookupGroup.getId(), this.lookupValue.getKeyValue());
+ } catch (LookupApiClientException e) {
+ // For structured errors that aren't NOT_FOUND
+ showError("Error checking for existing value: " + e.getError().getMessage());
+ return;
+ } catch (Exception e) {
+ logger.error("Unexpected error while checking existing value", e);
+ showError("Unexpected error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
+ return;
+ }
+
+ if (exists) {
+ int choice = JOptionPane.showConfirmDialog(
+ this,
+ "A value with the same key already exists. Do you want to overwrite it?",
+ "Confirm Overwrite",
+ JOptionPane.YES_NO_OPTION,
+ JOptionPane.WARNING_MESSAGE
+ );
+
+ if (choice != JOptionPane.YES_OPTION) {
+ return; // Cancel save
+ }
+ }
+ }
+
+ LookupValueResponse response = LookupServiceClient.getInstance().setValue(this.lookupGroup.getId(), this.lookupValue);
+
+ this.lookupValue.setKeyValue(response.getKey());
+ this.lookupValue.setValueData(response.getValue());
+ this.lookupValue.setCreatedDate(response.getCreatedDate());
+ this.lookupValue.setUpdatedDate(response.getUpdatedDate());
+
+ this.saved = true;
+
+ close();
+ } catch (LookupApiClientException e) {
+ showError(e.getError().getMessage());
+ } catch (Exception e) {
+ logger.error("Unexpected error while saving value", e);
+ showError("Unexpected error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
+ }
+ }
+
+ private void close() {
+ dispose();
+ }
+
+ public boolean isSaved() {
+ return saved;
+ }
+
+ private boolean validateProperties() {
+ boolean valid = true;
+ StringBuilder errorMessage = new StringBuilder();
+
+ // Reset backgrounds
+ resetInvalidComponents();
+
+ String name = keyField.getText().trim();
+ if (StringUtils.isEmpty(name)) {
+ valid = false;
+ keyField.setBackground(UIConstants.INVALID_COLOR);
+ errorMessage.append("Please provide a key.")
+ .append(System.lineSeparator());
+ }
+
+ String version = valueField.getText().trim();
+ if (StringUtils.isEmpty(version)) {
+ valid = false;
+ valueField.setBackground(UIConstants.INVALID_COLOR);
+ errorMessage.append("Please provide a value.")
+ .append(System.lineSeparator());
+ }
+
+ if (!valid) {
+ showError(errorMessage.toString());
+ }
+
+ return valid;
+ }
+
+ public void resetInvalidComponents() {
+ keyField.setBackground(null);
+ valueField.setBackground(null);
+ }
+
+ protected void showInformation(String msg) {
+ PlatformUI.MIRTH_FRAME.alertInformation(this, msg);
+ }
+
+ protected void showError(String err) {
+ PlatformUI.MIRTH_FRAME.alertError(this, err);
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/exception/LookupApiClientException.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/exception/LookupApiClientException.java
new file mode 100644
index 000000000..1f42a7ec3
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/exception/LookupApiClientException.java
@@ -0,0 +1,27 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.exception;
+
+import com.mirth.connect.client.core.ClientException;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.ErrorResponse;
+
+public class LookupApiClientException extends ClientException {
+ private final ErrorResponse error;
+
+ public LookupApiClientException(ErrorResponse error, Throwable cause) {
+ super(error.getMessage(), cause);
+ this.error = error;
+ }
+
+ public ErrorResponse getError() {
+ return error;
+ }
+}
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/model/LookupAuditTableModel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/model/LookupAuditTableModel.java
new file mode 100644
index 000000000..52d7f0e0f
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/model/LookupAuditTableModel.java
@@ -0,0 +1,81 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.model;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.GroupAuditEntriesResponse;
+
+import javax.swing.table.AbstractTableModel;
+import java.util.ArrayList;
+import java.util.List;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class LookupAuditTableModel extends AbstractTableModel {
+ private final String[] columnNames = {"Key", "Action", "Old Value", "New Value", "User", "Timestamp"};
+ private final List values = new ArrayList<>();
+
+ private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ @Override
+ public int getRowCount() {
+ return values.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return columnNames.length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ return columnNames[column];
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ GroupAuditEntriesResponse.AuditEntryResponse entry = values.get(rowIndex);
+ switch (columnIndex) {
+ case 0:
+ return entry.getKeyValue();
+ case 1:
+ return entry.getAction();
+ case 2:
+ return entry.getOldValue();
+ case 3:
+ return entry.getNewValue();
+ case 4:
+ return entry.getUserName();
+ case 5:
+ Date timestamp = entry.getTimestamp();
+ return timestamp != null ? formatter.format(timestamp) : "";
+ default:
+ return null;
+ }
+ }
+
+ public GroupAuditEntriesResponse.AuditEntryResponse getValue(int rowIndex) {
+ return values.get(rowIndex);
+ }
+
+ public void setValues(List newValues) {
+ values.clear();
+ values.addAll(newValues);
+ fireTableDataChanged();
+ }
+
+ public void clear() {
+ values.clear();
+ fireTableDataChanged();
+ }
+}
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/model/LookupGroupTableModel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/model/LookupGroupTableModel.java
new file mode 100644
index 000000000..9268e8b36
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/model/LookupGroupTableModel.java
@@ -0,0 +1,154 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.model;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+
+import javax.swing.table.AbstractTableModel;
+
+import java.text.SimpleDateFormat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class LookupGroupTableModel extends AbstractTableModel {
+ private final String[] columnNames = {"Group Name", "Updated Date"};
+ private final List allGroups = new ArrayList<>();
+ private List filteredGroups = new ArrayList<>();
+ private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd hh:mm a");
+ private String lastFilterText = "";
+
+ @Override
+ public int getRowCount() {
+ return filteredGroups.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return columnNames.length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ return columnNames[column];
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ LookupGroup group = filteredGroups.get(rowIndex);
+ switch (columnIndex) {
+ case 0:
+ return group.getName();
+ case 1:
+ return formatter.format(group.getUpdatedDate());
+ default:
+ return null;
+ }
+ }
+
+ public void addGroup(LookupGroup group) {
+ allGroups.add(group);
+ reapplyFilter();
+ fireTableDataChanged();
+ }
+
+ public void removeGroup(int rowIndex) {
+ LookupGroup group = filteredGroups.get(rowIndex);
+ allGroups.remove(group);
+ reapplyFilter();
+ fireTableDataChanged();
+ }
+
+ public void updateGroupById(LookupGroup group) {
+ int index = getIndexByGroupId(group.getId());
+ if (index >= 0) {
+ allGroups.set(index, group);
+ reapplyFilter();
+ fireTableDataChanged();
+ }
+ }
+
+ public void addOrUpdateGroup(LookupGroup group) {
+ int index = getIndexByGroupId(group.getId());
+ if (index >= 0) {
+ allGroups.set(index, group);
+ } else {
+ allGroups.add(group);
+ }
+ reapplyFilter();
+ fireTableDataChanged();
+ }
+
+ public LookupGroup getGroup(int rowIndex) {
+ return filteredGroups.get(rowIndex);
+ }
+
+ public void setGroups(List groups) {
+ allGroups.clear();
+ allGroups.addAll(groups);
+ reapplyFilter();
+ fireTableDataChanged();
+ }
+
+ public List getAllGroups() {
+ return new ArrayList<>(allGroups);
+ }
+
+ public void clear() {
+ allGroups.clear();
+ filteredGroups.clear();
+ fireTableDataChanged();
+ }
+
+ public void setFilter(String filterText) {
+ lastFilterText = filterText != null ? filterText.trim().toLowerCase() : "";
+ if (lastFilterText.isEmpty()) {
+ filteredGroups = new ArrayList<>(allGroups);
+ } else {
+ filteredGroups = allGroups.stream()
+ .filter(group -> group.getName().toLowerCase().contains(lastFilterText))
+ .collect(Collectors.toList());
+ }
+ fireTableDataChanged();
+ }
+
+ public void clearFilter() {
+ setFilter("");
+ }
+
+ public void reapplyFilter() {
+ setFilter(lastFilterText);
+ }
+
+ public int getIndexByGroupId(int id) {
+ for (int i = 0; i < allGroups.size(); i++) {
+ if (allGroups.get(i).getId() == id) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public int getFilteredIndexByGroupId(int id) {
+ for (int i = 0; i < filteredGroups.size(); i++) {
+ if (filteredGroups.get(i).getId() == id) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public boolean containsGroupId(int id) {
+ return getIndexByGroupId(id) >= 0;
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/model/LookupValueTableModel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/model/LookupValueTableModel.java
new file mode 100644
index 000000000..94f5b2c74
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/model/LookupValueTableModel.java
@@ -0,0 +1,97 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.model;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+
+import javax.swing.table.AbstractTableModel;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+public class LookupValueTableModel extends AbstractTableModel {
+ public static final int KEY_COLUMN = 0;
+ public static final int VALUE_COLUMN = 1;
+ public static final int UPDATED_DATE_COLUMN = 2;
+ public static final int ACTION_COLUMN = 3;
+
+ private final String[] columnNames = {"Key", "Value", "Updated Date", "Action"};
+ private final List values = new ArrayList<>();
+ private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd hh:mm a");
+
+ @Override
+ public int getRowCount() {
+ return values.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return columnNames.length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ return columnNames[column];
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ LookupValue value = values.get(rowIndex);
+ switch (columnIndex) {
+ case 0:
+ return value.getKeyValue();
+ case 1:
+ return value.getValueData();
+ case 2:
+ return formatter.format(value.getUpdatedDate());
+ case 3:
+ return null;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ return columnIndex == ACTION_COLUMN;
+ }
+
+ public void addValue(LookupValue value) {
+ values.add(value);
+ fireTableDataChanged();
+ }
+
+ public void removeValue(int rowIndex) {
+ values.remove(rowIndex);
+ fireTableDataChanged();
+ }
+
+ public void updateValue(int rowIndex, LookupValue newValue) {
+ values.set(rowIndex, newValue);
+ fireTableDataChanged();
+ }
+
+ public LookupValue getValue(int rowIndex) {
+ return values.get(rowIndex);
+ }
+
+ public void setValues(List newValues) {
+ values.clear();
+ values.addAll(newValues);
+ fireTableDataChanged();
+ }
+
+ public void clear() {
+ values.clear();
+ fireTableDataChanged();
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ButtonEditor.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ButtonEditor.java
new file mode 100644
index 000000000..3f3b4e7db
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ButtonEditor.java
@@ -0,0 +1,89 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import com.mirth.connect.client.ui.PlatformUI;
+import com.mirth.connect.plugins.dynamiclookup.client.model.LookupValueTableModel;
+
+import javax.swing.JTable;
+
+import javax.swing.event.CellEditorListener;
+import javax.swing.table.TableCellEditor;
+
+import java.awt.Frame;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import java.util.EventObject;
+
+public class ButtonEditor extends ButtonPanel implements TableCellEditor {
+ private final Frame parent = PlatformUI.MIRTH_FRAME;
+ private final LookupValueTableModel model;
+ private int currentRow;
+ private transient ActionListener editListener;
+ private transient ActionListener removeListener;
+
+ public ButtonEditor(JTable table, LookupValueTableModel model, ActionListener editListener, ActionListener removeListener) {
+ this.model = model;
+ this.editListener = e -> editListener.actionPerformed(new ActionEvent(currentRow, e.getID(), e.getActionCommand()));
+ this.removeListener = e -> removeListener.actionPerformed(new ActionEvent(currentRow, e.getID(), e.getActionCommand()));
+ this.currentRow = -1;
+
+ getEditButton().addActionListener(this.editListener);
+ getRemoveButton().addActionListener(this.removeListener);
+ }
+
+ @Override
+ public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
+ currentRow = row;
+ if (isSelected) {
+ setBackground(table.getSelectionBackground());
+ setForeground(table.getSelectionForeground());
+ } else {
+ setBackground(table.getBackground());
+ setForeground(table.getForeground());
+ }
+ return this;
+ }
+
+ @Override
+ public Object getCellEditorValue() {
+ return null;
+ }
+
+ @Override
+ public boolean isCellEditable(EventObject anEvent) {
+ return true;
+ }
+
+ @Override
+ public boolean shouldSelectCell(EventObject anEvent) {
+ return true;
+ }
+
+ @Override
+ public boolean stopCellEditing() {
+ return true;
+ }
+
+ @Override
+ public void cancelCellEditing() {
+ }
+
+ @Override
+ public void addCellEditorListener(CellEditorListener l) {
+ }
+
+ @Override
+ public void removeCellEditorListener(CellEditorListener l) {
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ButtonPanel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ButtonPanel.java
new file mode 100644
index 000000000..f765a4043
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ButtonPanel.java
@@ -0,0 +1,42 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import javax.swing.JButton;
+import javax.swing.JPanel;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+
+public class ButtonPanel extends JPanel {
+ private final JButton editButton;
+ private final JButton removeButton;
+
+ public ButtonPanel() {
+ setLayout(new FlowLayout(FlowLayout.CENTER, 5, 0));
+ setOpaque(false);
+
+ editButton = new JButton("Edit");
+ editButton.setMargin(new Insets(2, 5, 2, 5));
+ removeButton = new JButton("Remove");
+ removeButton.setMargin(new Insets(2, 5, 2, 5));
+
+ add(editButton);
+ add(removeButton);
+ }
+
+ public JButton getEditButton() {
+ return editButton;
+ }
+
+ public JButton getRemoveButton() {
+ return removeButton;
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ButtonRenderer.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ButtonRenderer.java
new file mode 100644
index 000000000..28edc9cd3
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ButtonRenderer.java
@@ -0,0 +1,29 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import javax.swing.table.TableCellRenderer;
+import javax.swing.JTable;
+import java.awt.Component;
+
+public class ButtonRenderer extends ButtonPanel implements TableCellRenderer {
+ @Override
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
+ if (isSelected) {
+ setBackground(table.getSelectionBackground());
+ setForeground(table.getSelectionForeground());
+ } else {
+ setBackground(table.getBackground());
+ setForeground(table.getForeground());
+ }
+ return this;
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/CacheStatusPanel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/CacheStatusPanel.java
new file mode 100644
index 000000000..bcfdafb84
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/CacheStatusPanel.java
@@ -0,0 +1,336 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import com.mirth.connect.client.core.ClientException;
+import com.mirth.connect.client.ui.Frame;
+import com.mirth.connect.client.ui.PlatformUI;
+import com.mirth.connect.client.ui.UIConstants;
+
+import com.mirth.connect.plugins.dynamiclookup.client.service.LookupServiceClient;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.CacheStatistics;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.GroupStatisticsResponse;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+
+import net.miginfocom.swing.MigLayout;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.general.DefaultPieDataset;
+
+import javax.swing.JPanel;
+import javax.swing.JLabel;
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JOptionPane;
+import javax.swing.JScrollPane;
+
+import java.awt.CardLayout;
+import java.awt.GridLayout;
+import java.awt.FlowLayout;
+import java.awt.Dimension;
+
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+
+public class CacheStatusPanel extends JPanel {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+ private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd hh:mm a");
+
+ private final Frame parent = PlatformUI.MIRTH_FRAME;
+
+ private JPanel noGroupSelectedPanel;
+ private JPanel contentPanel;
+
+ private LookupGroup selectedGroup;
+
+ public CacheStatusPanel() {
+ initComponents();
+ initLayout();
+ }
+
+ private void initComponents() {
+ setBackground(UIConstants.BACKGROUND_COLOR);
+
+ // No group selected panel
+ noGroupSelectedPanel = new NoGroupSelectedPanel();
+ }
+
+ private void initLayout() {
+ setLayout(new CardLayout());
+
+ // Content panel with titled border
+ contentPanel = new JPanel(new MigLayout("insets 8, wrap 1, fill", "[grow]", "[]"));
+ contentPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+
+ add(contentPanel, "content");
+ add(noGroupSelectedPanel, "noGroup");
+ }
+
+ public void updateCaches(LookupGroup selectedGroup) {
+ boolean showContent = selectedGroup != null;
+
+ contentPanel.setVisible(showContent);
+ noGroupSelectedPanel.setVisible(!showContent);
+
+ this.selectedGroup = selectedGroup;
+
+ if (showContent) {
+ refreshUI();
+ }
+ }
+
+ private void refreshUI() {
+ contentPanel.removeAll();
+
+ GroupStatisticsResponse data = fetchGroupStatistics(selectedGroup);
+ if (data == null || data.getCacheStatistics() == null) {
+ showError("Failed to fetch cache statistics.");
+
+ contentPanel.revalidate();
+ contentPanel.repaint();
+
+ return;
+ }
+
+ CacheStatistics stats = data.getCacheStatistics();
+
+ // Create horizontal layout
+ JPanel horizontalPanel = new JPanel(new MigLayout("insets 0, fill, gapx 5", "[220!][grow, fill]", "[]"));
+ horizontalPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+
+ // LEFT: Text Info Panel
+ JPanel textInfoPanel = createTextInfoPanel(data);
+ horizontalPanel.add(textInfoPanel, "growy");
+
+ // RIGHT: Chart Stack Panel
+ JPanel chartsPanel = new JPanel(new MigLayout("insets 0, wrap 1", "[grow, fill]", "[]10[]10[]"));
+ chartsPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ chartsPanel.setBorder(BorderFactory.createTitledBorder("Statistics Visualizations"));
+
+ // 1. Bar Chart: Cache Lookups Breakdown
+ long totalLookups = data.getTotalLookups();
+ long cacheHits = data.getCacheHits();
+ long nonHits = totalLookups - cacheHits;
+
+ DefaultCategoryDataset lookupDataset = new DefaultCategoryDataset();
+ lookupDataset.addValue(cacheHits, "Lookups", "Cache Hits");
+ lookupDataset.addValue(nonHits, "Lookups", "Misses");
+
+ JFreeChart lookupChart = ChartFactory.createBarChart(
+ "Group Lookups Breakdown",
+ "Result Type",
+ "Count",
+ lookupDataset,
+ PlotOrientation.VERTICAL,
+ false, true, false
+ );
+ ChartPanel lookupChartPanel = new ChartPanel(lookupChart);
+ lookupChartPanel.setPreferredSize(new Dimension(300, 200));
+ chartsPanel.add(lookupChartPanel, "growx, wrap");
+
+ // 2. Bar Chart: Current Entries vsConfigured Max
+ DefaultCategoryDataset sizeDataset = new DefaultCategoryDataset();
+ sizeDataset.addValue(stats.getCurrentEntryCount(), "Cache", "Current Entries");
+ sizeDataset.addValue(stats.getConfiguredMaxEntries(), "Cache", "Configured Max");
+
+ JFreeChart sizeBarChart = ChartFactory.createBarChart(
+ "Cache Entry Count",
+ "Category",
+ "Entries",
+ sizeDataset,
+ PlotOrientation.VERTICAL,
+ false, true, false
+ );
+ ChartPanel sizeBarChartPanel = new ChartPanel(sizeBarChart);
+ sizeBarChartPanel.setPreferredSize(new Dimension(300, 200));
+ chartsPanel.add(sizeBarChartPanel, "growx, wrap");
+
+ // 3. Pie Chart: Cache Hit vs Miss Rate (only if stats are supported)
+ if (!stats.isStatsSupported()) {
+ JPanel unsupportedPanel = new JPanel();
+ unsupportedPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ unsupportedPanel.add(new JLabel(
+ "Hit/Miss distribution is not available for this eviction policy."
+ ));
+ chartsPanel.add(unsupportedPanel);
+
+ } else if (stats.getHitCount() > 0 || stats.getMissCount() > 0) {
+ DefaultPieDataset pieDataset = new DefaultPieDataset();
+ pieDataset.setValue("Hits", stats.getHitCount());
+ pieDataset.setValue("Misses", stats.getMissCount());
+
+ JFreeChart pieChart = ChartFactory.createPieChart(
+ "Hit vs Miss Distribution",
+ pieDataset,
+ true, true, false
+ );
+ ChartPanel pieChartPanel = new ChartPanel(pieChart);
+ pieChartPanel.setPreferredSize(new Dimension(300, 200));
+ chartsPanel.add(pieChartPanel, "growx, wrap");
+
+ } else {
+ JPanel emptyStatsPanel = new JPanel();
+ emptyStatsPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ emptyStatsPanel.add(new JLabel(
+ "Hit/Miss distribution will appear after lookups occur."
+ ));
+ chartsPanel.add(emptyStatsPanel);
+ }
+
+ // Add both panels to the content
+ JScrollPane chartScroll = new JScrollPane(chartsPanel);
+ chartScroll.setBorder(null);
+ chartScroll.setPreferredSize(new Dimension(400, 600)); // adjust as needed
+ horizontalPanel.add(chartScroll, "grow, wrap");
+
+ contentPanel.add(horizontalPanel, "span, grow, push");
+
+ contentPanel.revalidate();
+ contentPanel.repaint();
+ }
+
+ private GroupStatisticsResponse fetchGroupStatistics(LookupGroup group) {
+ try {
+ return LookupServiceClient.getInstance().getGroupStatistics(group.getId());
+ } catch (ClientException e) {
+ logger.error("Failed to fetch group statistics for group ID {}", group.getId(), e);
+ showError("Unable to fetch group statistics for the selected group:\n" + e.getMessage());
+ return null;
+ }
+ }
+
+ private void resetGroupStatistics(LookupGroup group) {
+ try {
+ LookupServiceClient.getInstance().resetGroupStatistics(group.getId());
+ } catch (ClientException e) {
+ logger.error("Failed to reset group statistics for group ID {}", group.getId(), e);
+ showError("Unable to reset group statistics for the selected group:\n" + e.getMessage());
+ }
+ }
+
+ private void clearGroupCache(LookupGroup group) {
+ try {
+ LookupServiceClient.getInstance().clearGroupCache(group.getId());
+ } catch (ClientException e) {
+ logger.error("Failed to clear group cache for group ID {}", group.getId(), e);
+ showError("Unable to clear group cache for the selected group:\n" + e.getMessage());
+ }
+ }
+
+ private JPanel createTextInfoPanel(GroupStatisticsResponse data) {
+ JPanel mainPanel = new JPanel(new GridLayout(0, 1, 10, 10));
+ mainPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+
+ DecimalFormat numberFormat = new DecimalFormat("#,###");
+ DecimalFormat percentFormat = new DecimalFormat("0.00%");
+
+ // Top: Lookup Group Stats
+ JPanel groupPanel = new JPanel(new MigLayout("insets 8, wrap 1, fillx", "[left]", "[]5"));
+ groupPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ groupPanel.setBorder(BorderFactory.createTitledBorder("Lookup Group Statistics"));
+ groupPanel.setToolTipText("These statistics are retrieved from the database (persistent values).");
+
+ groupPanel.add(new JLabel("Group ID: " + data.getGroupId()));
+ groupPanel.add(new JLabel("Total Lookups: " + numberFormat.format(data.getTotalLookups())));
+ groupPanel.add(new JLabel("Cache Hits: " + numberFormat.format(data.getCacheHits())));
+ groupPanel.add(new JLabel("Hit Rate (Group Level): " + percentFormat.format(data.getHitRate())));
+ groupPanel.add(new JLabel("Last Accessed: " + formatDisplayDate(data.getLastAccessed())));
+ groupPanel.add(new JLabel("Reset Date: " + formatDisplayDate(data.getResetDate())));
+
+ // Bottom: Cache Stats
+ CacheStatistics stats = data.getCacheStatistics();
+ JPanel cachePanel = new JPanel(new MigLayout("insets 8, wrap 1, fillx", "[left]", "[]5"));
+ cachePanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ cachePanel.setBorder(BorderFactory.createTitledBorder("Cache Performance"));
+ cachePanel.setToolTipText("These statistics come from in-memory cache metrics (real-time values).");
+
+ cachePanel.add(new JLabel("Eviction Policy: " + stats.getEvictionPolicy()));
+ cachePanel.add(new JLabel("Current Entry Count: " + stats.getCurrentEntryCount()));
+ cachePanel.add(new JLabel("Configured Max Entries: " + stats.getConfiguredMaxEntries()));
+
+ if (stats.isStatsSupported()) {
+ cachePanel.add(new JLabel("Hit Count: " + numberFormat.format(stats.getHitCount())));
+ cachePanel.add(new JLabel("Miss Count: " + numberFormat.format(stats.getMissCount())));
+ cachePanel.add(new JLabel("Eviction Count: " + numberFormat.format(stats.getEvictionCount())));
+ cachePanel.add(new JLabel("Hit Ratio (Cache Level): " + percentFormat.format(stats.getHitRatio())));
+ cachePanel.add(new JLabel("Miss Ratio (Cache Level): " + percentFormat.format(stats.getMissRatio())));
+ cachePanel.add(new JLabel("Total Load Time: " + stats.getTotalLoadTimeFormatted()));
+ } else {
+ cachePanel.add(new JLabel("
Cache statistics are not supported for this eviction policy.
"));
+ }
+
+ mainPanel.add(groupPanel);
+ mainPanel.add(cachePanel);
+ mainPanel.add(buildActionButtonsPanel());
+
+ return mainPanel;
+ }
+
+ private JPanel buildActionButtonsPanel() {
+ JButton clearGroupStatsButton = new JButton("Reset Group Statistics");
+ clearGroupStatsButton.setToolTipText("Clears database-stored lookup counters for this group.");
+ clearGroupStatsButton.addActionListener(e -> {
+ int confirm = JOptionPane.showConfirmDialog(
+ this,
+ "Are you sure you want to reset group statistics?",
+ "Confirm Reset",
+ JOptionPane.YES_NO_OPTION
+ );
+ if (confirm == JOptionPane.YES_OPTION) {
+ resetGroupStatistics(selectedGroup);
+ refreshUI();
+ }
+ });
+
+ JButton clearCacheButton = new JButton("Clear Cache");
+ clearCacheButton.setToolTipText("Clears all entries and in-memory cache metrics for this group.");
+ clearCacheButton.addActionListener(e -> {
+ int confirm = JOptionPane.showConfirmDialog(
+ this,
+ "Are you sure you want to clear the in-memory cache?",
+ "Confirm Clear",
+ JOptionPane.YES_NO_OPTION
+ );
+ if (confirm == JOptionPane.YES_OPTION) {
+ clearGroupCache(selectedGroup);
+ refreshUI();
+ }
+ });
+
+ JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ buttonPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ buttonPanel.add(clearGroupStatsButton);
+ buttonPanel.add(clearCacheButton);
+
+ return buttonPanel;
+ }
+
+ private String formatDisplayDate(Date date) {
+ if (date == null) {
+ return "-";
+ }
+
+ return formatter.format(date);
+ }
+
+
+ private void showError(String err) {
+ PlatformUI.MIRTH_FRAME.alertError(parent, err);
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/DataStorePanel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/DataStorePanel.java
new file mode 100644
index 000000000..c06dd5148
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/DataStorePanel.java
@@ -0,0 +1,143 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import com.mirth.connect.client.ui.Frame;
+import com.mirth.connect.client.ui.PlatformUI;
+import org.jdesktop.swingx.JXTaskPane;
+import org.jdesktop.swingx.JXTaskPaneContainer;
+
+import javax.swing.JPanel;
+import javax.swing.ImageIcon;
+
+import java.awt.Component;
+import java.awt.event.ContainerAdapter;
+import java.awt.event.ContainerEvent;
+
+public class DataStorePanel extends JPanel {
+ private Frame parent;
+
+ private JXTaskPane dataStoreTasks;
+ private LookupTablePanel lookupTablePanel;
+ private boolean isReordering = false;
+
+ public DataStorePanel() {
+ this.parent = PlatformUI.MIRTH_FRAME;
+
+ dataStoreTasks = new CustomTaskPane(parent);
+ dataStoreTasks.setTitle("Data Store");
+ dataStoreTasks.setName("Data Store");
+ dataStoreTasks.setFocusable(false);
+
+ parent.addTask("doShowLookup", "Lookup Manager", "Contains information about Lookup Table.", "", new ImageIcon(com.mirth.connect.client.ui.Frame.class.getResource("images/table.png")), dataStoreTasks, null, this);
+ parent.setNonFocusable(dataStoreTasks);
+
+ dataStoreTasks.setVisible(true);
+ keepComponentBefore(parent.taskPaneContainer, dataStoreTasks, parent.otherPane);
+ }
+
+ private void keepComponentBefore(JXTaskPaneContainer container, Component toMove, Component target) {
+ container.addContainerListener(new ContainerAdapter() {
+ @Override
+ public void componentAdded(ContainerEvent e) {
+ ensureOrder(container, toMove, target);
+ }
+
+ @Override
+ public void componentRemoved(ContainerEvent e) {
+ ensureOrder(container, toMove, target);
+ }
+ });
+
+ // Initial call
+ ensureOrder(container, toMove, target);
+ }
+
+ private void ensureOrder(JXTaskPaneContainer container, Component toMove, Component target) {
+ if (isReordering) {
+ return;
+ }
+
+ isReordering = true;
+ try {
+ Component[] components = container.getComponents();
+ int moveIndex = -1;
+ int targetIndex = -1;
+
+ for (int i = 0; i < components.length; i++) {
+ if (components[i] == toMove) {
+ moveIndex = i;
+ } else if (components[i] == target) {
+ targetIndex = i;
+ }
+ }
+
+ if (targetIndex != -1 && (moveIndex == -1 || moveIndex != targetIndex - 1)) {
+ container.remove(toMove);
+
+ if (moveIndex != -1 && moveIndex < targetIndex) {
+ targetIndex--;
+ }
+
+ container.add(toMove, targetIndex);
+ container.revalidate();
+ container.repaint();
+ }
+ } finally {
+ isReordering = false;
+ }
+ }
+
+ public void unBold() {
+ parent.setBold(dataStoreTasks, -1);
+ }
+
+ public void doShowLookup() {
+ if (lookupTablePanel == null) {
+ lookupTablePanel = new LookupTablePanel(this);
+ }
+
+ if (!parent.confirmLeave()) {
+ return;
+ }
+
+ parent.setBold(parent.viewPane, -1);
+ parent.setBold(dataStoreTasks, 0);
+ parent.setPanelName("Lookup Manager");
+ parent.setCurrentContentPage(lookupTablePanel);
+ parent.setFocus(dataStoreTasks);
+
+ lookupTablePanel.doRefresh();
+ }
+
+ private class CustomTaskPane extends JXTaskPane {
+ private Frame parent;
+
+ public CustomTaskPane(Frame parent) {
+ this.parent = parent;
+ }
+
+ @Override
+ public void setVisible(boolean aFlag) {
+ if (!aFlag) {
+ // set to invisible during transformerPane and filterPane
+ if (parent.currentContentPage != null && parent.channelEditPanel != null && parent.channelEditPanel.transformerPane != null && parent.channelEditPanel.filterPane != null) {
+ if (parent.currentContentPage == parent.channelEditPanel.transformerPane || parent.currentContentPage == parent.channelEditPanel.filterPane) {
+ super.setVisible(false);
+ return;
+ }
+ }
+ }
+
+ super.setVisible(true);
+ }
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/DetailsPanel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/DetailsPanel.java
new file mode 100644
index 000000000..714dd29ef
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/DetailsPanel.java
@@ -0,0 +1,123 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import com.mirth.connect.client.ui.Frame;
+import com.mirth.connect.client.ui.PlatformUI;
+import com.mirth.connect.client.ui.UIConstants;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+
+import net.miginfocom.swing.MigLayout;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.swing.JPanel;
+import javax.swing.JTextArea;
+import javax.swing.BorderFactory;
+import javax.swing.JScrollPane;
+
+import java.awt.Font;
+import java.awt.CardLayout;
+import java.awt.Dimension;
+
+import java.text.SimpleDateFormat;
+
+public class DetailsPanel extends JPanel {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+ private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd hh:mm a");
+
+ private final Frame parent = PlatformUI.MIRTH_FRAME;
+
+ private JPanel noGroupSelectedPanel;
+ private JPanel contentPanel;
+ private JTextArea detailsTextArea;
+
+ private LookupGroup selectedGroup;
+
+ public DetailsPanel() {
+ initComponents();
+ initLayout();
+ }
+
+ private void initComponents() {
+ setBackground(UIConstants.BACKGROUND_COLOR);
+
+ noGroupSelectedPanel = new NoGroupSelectedPanel();
+
+ detailsTextArea = new JTextArea();
+ detailsTextArea.setEditable(false);
+ detailsTextArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+ detailsTextArea.setBackground(UIConstants.BACKGROUND_COLOR);
+ detailsTextArea.setLineWrap(false);
+ detailsTextArea.setWrapStyleWord(false);
+ detailsTextArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+ }
+
+ private void initLayout() {
+ setLayout(new CardLayout());
+
+ contentPanel = new JPanel(new MigLayout("insets 8, wrap 1, fillx", "[grow]", "[]"));
+ contentPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+
+ JScrollPane scrollPane = new JScrollPane(detailsTextArea);
+ scrollPane.setBorder(BorderFactory.createEmptyBorder());
+ scrollPane.setPreferredSize(new Dimension(500, 200));
+
+ // Place at top, do not stretch vertically
+ contentPanel.add(scrollPane, "aligny top, growx, wrap");
+
+ add(contentPanel, "content");
+ add(noGroupSelectedPanel, "noGroup");
+ }
+
+
+ public void updateDetails(LookupGroup selectedGroup) {
+ boolean showContent = selectedGroup != null;
+
+ contentPanel.setVisible(showContent);
+ noGroupSelectedPanel.setVisible(!showContent);
+
+ this.selectedGroup = selectedGroup;
+
+ if (showContent) {
+ refreshUI();
+ }
+ }
+
+ private void refreshUI() {
+ if (selectedGroup == null) {
+ detailsTextArea.setText("");
+ return;
+ }
+
+ String details = String.format(
+ "%-15s: %d%n" +
+ "%-15s: %s%n" +
+ "%-15s: %s%n" +
+ "%-15s: %s%n" +
+ "%-15s: %d%n" +
+ "%-15s: %s%n" +
+ "%-15s: %s%n" +
+ "%-15s: %s%n",
+ "ID", selectedGroup.getId(),
+ "Name", selectedGroup.getName(),
+ "Description", selectedGroup.getDescription() != null ? selectedGroup.getDescription() : "",
+ "Version", selectedGroup.getVersion() != null ? selectedGroup.getVersion() : "",
+ "Cache Size", selectedGroup.getCacheSize(),
+ "Cache Policy", selectedGroup.getCachePolicy() != null ? selectedGroup.getCachePolicy() : "",
+ "Created Date", formatter.format(selectedGroup.getCreatedDate()),
+ "Updated Date", formatter.format(selectedGroup.getUpdatedDate())
+ );
+
+ detailsTextArea.setText(details);
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/GroupPanel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/GroupPanel.java
new file mode 100644
index 000000000..5e6d62415
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/GroupPanel.java
@@ -0,0 +1,640 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.mirth.connect.client.ui.Frame;
+import com.mirth.connect.client.ui.PlatformUI;
+import com.mirth.connect.client.ui.UIConstants;
+import com.mirth.connect.plugins.dynamiclookup.client.dialog.LookupGroupDialog;
+import com.mirth.connect.plugins.dynamiclookup.client.exception.LookupApiClientException;
+import com.mirth.connect.plugins.dynamiclookup.client.model.LookupGroupTableModel;
+import com.mirth.connect.plugins.dynamiclookup.client.service.LookupServiceClient;
+import com.mirth.connect.plugins.dynamiclookup.client.util.FileChooser;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.request.ImportLookupGroupRequest;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.ExportGroupPagedResponse;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.ExportLookupGroupResponse;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.ImportLookupGroupResponse;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+import com.mirth.connect.plugins.dynamiclookup.shared.util.JsonUtils;
+
+import net.miginfocom.swing.MigLayout;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.swing.JPanel;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPopupMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JScrollPane;
+import javax.swing.SwingWorker;
+import javax.swing.JOptionPane;
+import javax.swing.JFileChooser;
+import javax.swing.JDialog;
+import javax.swing.JProgressBar;
+import javax.swing.BorderFactory;
+import javax.swing.ListSelectionModel;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.ListSelectionListener;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+import java.io.File;
+import java.io.IOException;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * @author Thai Tran (thaitran@innovarhealthcare.com)
+ * @create 2025-05-13 10:25 AM
+ */
+public class GroupPanel extends JPanel {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ private final Frame parent = PlatformUI.MIRTH_FRAME;
+ private JTextField groupFilterField;
+ private JButton addGroupButton;
+ private JButton importButton;
+ private JTable groupTable;
+ private LookupGroupTableModel groupTableModel;
+ private JPopupMenu groupPopupMenu;
+
+ public GroupPanel() {
+ initComponents();
+ initLayout();
+ }
+
+ private void initComponents() {
+ setBackground(UIConstants.BACKGROUND_COLOR);
+
+ // Group Filter
+ groupFilterField = new JTextField();
+ groupFilterField.setToolTipText("Filter groups by name");
+ groupFilterField.getDocument().addDocumentListener(new DocumentListener() {
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ filterGroups();
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ filterGroups();
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ filterGroups();
+ }
+ });
+
+ // Group Buttons
+ addGroupButton = new JButton("Add");
+ addGroupButton.addActionListener(e -> handleAddGroup());
+
+ // Import button
+ importButton = new JButton("Import JSON");
+ importButton.setToolTipText("Import a lookup group and its values from a JSON file");
+ importButton.addActionListener(e -> handleImportJson());
+
+ // Group Table
+ groupTableModel = new LookupGroupTableModel();
+ groupTable = new JTable(groupTableModel);
+ groupTable.setRowHeight(26);
+ groupTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+ // Popup Menu
+ groupPopupMenu = new JPopupMenu();
+ JMenuItem editItem = new JMenuItem("Edit");
+ editItem.addActionListener(e -> handleEditGroup());
+ JMenuItem removeItem = new JMenuItem("Remove");
+ removeItem.addActionListener(e -> handleDeleteGroup());
+ JMenuItem exportItem = new JMenuItem("Export JSON");
+ exportItem.addActionListener(e -> handleExportJson());
+
+ groupPopupMenu.add(editItem);
+ groupPopupMenu.add(removeItem);
+ groupPopupMenu.add(exportItem);
+
+ // Attach popup menu to table
+ groupTable.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ if (e.isPopupTrigger()) {
+ showPopup(e);
+ }
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ if (e.isPopupTrigger()) {
+ showPopup(e);
+ }
+ }
+
+ private void showPopup(MouseEvent e) {
+ int row = groupTable.rowAtPoint(e.getPoint());
+ if (row >= 0) {
+ groupTable.setRowSelectionInterval(row, row);
+ groupPopupMenu.show(groupTable, e.getX(), e.getY());
+ }
+ }
+ });
+ }
+
+ // In GroupPanel.java, update initLayout
+ private void initLayout() {
+ setLayout(new MigLayout("insets 0, fill"));
+
+ JPanel contentPanel = new JPanel(new MigLayout("insets 8, fill"));
+ contentPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ contentPanel.setBorder(BorderFactory.createTitledBorder("Lookup Groups"));
+
+ contentPanel.add(groupFilterField, "w 150!, growx, split 3");
+ contentPanel.add(addGroupButton);
+ contentPanel.add(importButton, "wrap");
+ contentPanel.add(new JScrollPane(groupTable), "grow, push, wrap");
+
+ add(contentPanel, "grow, push");
+ }
+
+ private void filterGroups() {
+ String filterText = groupFilterField.getText().trim().toLowerCase();
+ groupTableModel.setFilter(filterText);
+ }
+
+ private void handleAddGroup() {
+ LookupGroup lookupGroup = new LookupGroup();
+ LookupGroupDialog dialog = new LookupGroupDialog(parent, lookupGroup, false);
+ if (dialog.isSaved()) {
+ int selectedRow = groupTable.getSelectedRow();
+ groupTableModel.addGroup(lookupGroup);
+ if (selectedRow >= 0 && selectedRow < groupTableModel.getRowCount()) {
+ groupTable.setRowSelectionInterval(selectedRow, selectedRow);
+ }
+ }
+ }
+
+ private void handleImportJson() {
+ JFileChooser importFileChooser = new JFileChooser();
+ importFileChooser.setFileFilter(new FileNameExtensionFilter("JSON files", "json"));
+
+ File currentDir = new File(Frame.userPreferences.get("currentDirectory", ""));
+ if (currentDir.exists()) {
+ importFileChooser.setCurrentDirectory(currentDir);
+ }
+
+ if (importFileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
+ File file = importFileChooser.getSelectedFile();
+ if (file == null || !checkJsonFile(file)) return;
+
+ int result = JOptionPane.showConfirmDialog(
+ parent,
+ "If the group you're importing already exists, "
+ + "its information will be updated and all existing values will be permanently deleted and replaced.
"
+ + "Do you want to proceed with updating the group?",
+ "Confirm Import Overwrite",
+ JOptionPane.YES_NO_OPTION,
+ JOptionPane.WARNING_MESSAGE
+ );
+
+ if (result != JOptionPane.YES_OPTION) return;
+
+ JDialog progressDialog = new JDialog(parent, "Importing JSON", true);
+ JProgressBar progressBar = new JProgressBar(0, 100);
+ progressBar.setStringPainted(true);
+ JLabel statusLabel = new JLabel("Imported 0 entries");
+ JButton cancelButton = new JButton("Cancel");
+ JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+ buttonPanel.add(cancelButton);
+
+ progressDialog.setLayout(new BorderLayout(10, 10));
+ progressDialog.add(statusLabel, BorderLayout.NORTH);
+ progressDialog.add(progressBar, BorderLayout.CENTER);
+ progressDialog.add(buttonPanel, BorderLayout.SOUTH);
+ progressDialog.setSize(350, 120);
+ progressDialog.setLocationRelativeTo(parent);
+ progressDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
+
+ SwingWorker importWorker = new SwingWorker() {
+ int finalGroupId = -1;
+
+ @Override
+ protected Void doInBackground() throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode root = mapper.readTree(file);
+ int totalCount = 0;
+ JsonNode valuesNode = root.get("values");
+ if (valuesNode != null && valuesNode.isObject()) {
+ totalCount = valuesNode.size();
+ }
+
+ try (JsonParser parser = mapper.getFactory().createParser(file)) {
+ int groupId = -1;
+ boolean groupImported = false;
+ int batchSize = 1000;
+ int processed = 0;
+ Map batch = new LinkedHashMap<>();
+
+ JsonToken token = parser.nextToken(); // Advance to first token
+ if (token != JsonToken.START_OBJECT) {
+ throw new IllegalStateException("Expected START_OBJECT at beginning of JSON file.");
+ }
+
+ while (parser.nextToken() != JsonToken.END_OBJECT) {
+ String fieldName = parser.getCurrentName();
+ if ("group".equals(fieldName)) {
+ parser.nextToken();
+ JsonNode groupNode = parser.readValueAsTree();
+
+ ImportLookupGroupRequest request = new ImportLookupGroupRequest();
+ request.setGroup(JsonUtils.fromJson(groupNode.toString(), LookupGroup.class));
+ request.setValues(Collections.emptyMap());
+ request.validate();
+
+ ImportLookupGroupResponse response = LookupServiceClient.getInstance().importGroup(true, JsonUtils.toJson(request));
+ groupId = response.getGroupId();
+ groupImported = true;
+ finalGroupId = groupId;
+ } else if ("values".equals(fieldName)) {
+ if (!groupImported) {
+ throw new IllegalStateException("JSON file does not contain a valid 'group' object.");
+ }
+ parser.nextToken();
+ while (parser.nextToken() != JsonToken.END_OBJECT && !isCancelled()) {
+ String key = parser.getCurrentName();
+ parser.nextToken();
+ String value = parser.getValueAsString();
+ batch.put(key, value);
+ processed++;
+
+ if (batch.size() >= batchSize) {
+ LookupServiceClient.getInstance().importValues(groupId, false, batch);
+ batch.clear();
+ publishProgress(processed, totalCount);
+ }
+ }
+
+ if (!batch.isEmpty() && !isCancelled()) {
+ LookupServiceClient.getInstance().importValues(groupId, false, batch);
+ publishProgress(processed, totalCount);
+ }
+ } else {
+ parser.skipChildren();
+ }
+ }
+
+ Frame.userPreferences.put("currentDirectory", file.getParent());
+ } catch (Exception e) {
+ logger.error("Failed to import lookup group from JSON file", e);
+ throw e;
+ }
+
+ return null;
+ }
+
+ private void publishProgress(int processed, int total) {
+ int progress = total > 0 ? (int) ((processed / (double) total) * 100) : 0;
+ progress = Math.min(progress, 100);
+
+ publish(new int[]{progress, processed, total});
+ }
+
+ @Override
+ protected void process(List chunks) {
+ if (!chunks.isEmpty()) {
+ int[] latest = chunks.get(chunks.size() - 1);
+ progressBar.setIndeterminate(false);
+ progressBar.setValue(latest[0]);
+ statusLabel.setText("Imported " + latest[1] + " of " + latest[2] + " entries");
+ }
+ }
+
+ @Override
+ protected void done() {
+ progressDialog.dispose();
+
+ if (isCancelled()) {
+ JOptionPane.showMessageDialog(parent, "Import cancelled by user.", "Import Cancelled", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+
+ try {
+ get(); // triggers exception handling if doInBackground failed
+
+ JOptionPane.showMessageDialog(parent, "Import completed successfully.", "Import Complete", JOptionPane.INFORMATION_MESSAGE);
+
+ // Update table
+ if (finalGroupId != -1) {
+ LookupGroup selectedGroup = groupTable.getSelectedRow() >= 0
+ ? groupTableModel.getGroup(groupTable.getSelectedRow())
+ : null;
+ try {
+ LookupGroup importedGroup = LookupServiceClient.getInstance().getGroupById(finalGroupId);
+ groupTableModel.addOrUpdateGroup(importedGroup);
+
+ if (selectedGroup != null) {
+ int visibleIndex = groupTableModel.getFilteredIndexByGroupId(selectedGroup.getId());
+ if (visibleIndex >= 0) {
+ groupTable.setRowSelectionInterval(visibleIndex, visibleIndex);
+ groupTable.scrollRectToVisible(groupTable.getCellRect(visibleIndex, 0, true));
+ }
+ }
+ } catch (Exception ex) {
+ logger.warn("Imported group added, but failed to update table", ex);
+ }
+ }
+
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ showError("Import failed: " + (cause.getMessage() != null ? cause.getMessage() : cause.getClass().getSimpleName()));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // restore interrupt status
+ showError("Import was interrupted.");
+ }
+ }
+ };
+
+ cancelButton.addActionListener(e -> importWorker.cancel(true));
+ progressDialog.addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent e) {
+ if (JOptionPane.showConfirmDialog(progressDialog, "Cancel import?", "Confirm", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
+ importWorker.cancel(true);
+ }
+ }
+ });
+
+ importWorker.execute();
+ progressDialog.setVisible(true);
+ }
+ }
+
+ private boolean checkJsonFile(File file) {
+ // 1. Extension check
+ if (!file.getName().toLowerCase().endsWith(".json")) {
+ showError("File does not have a .json extension.");
+ return false;
+ }
+
+ // 2. MIME type check
+ try {
+ Path path = file.toPath();
+ String mimeType = Files.probeContentType(path);
+ if (mimeType == null || !(
+ mimeType.equals("application/json") ||
+ mimeType.equals("text/plain")
+ )) {
+ showError("File does not appear to be a valid JSON (detected type: " + mimeType + ").");
+ return false;
+ }
+ } catch (IOException e) {
+ showError("Failed to detect file type: " + e.getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ private void handleEditGroup() {
+ int selectedRow = groupTable.getSelectedRow();
+ if (selectedRow >= 0) {
+ LookupGroup group = groupTableModel.getGroup(selectedRow);
+ LookupGroup copy = new LookupGroup(group);
+ LookupGroupDialog dialog = new LookupGroupDialog(parent, copy, true);
+ if (dialog.isSaved()) {
+ groupTableModel.updateGroupById(copy);
+
+ // Reselect and scroll to updated row if it's still visible
+ int visibleIndex = groupTableModel.getFilteredIndexByGroupId(copy.getId());
+ if (visibleIndex >= 0) {
+ groupTable.setRowSelectionInterval(visibleIndex, visibleIndex);
+ groupTable.scrollRectToVisible(groupTable.getCellRect(visibleIndex, 0, true));
+ }
+ }
+ }
+ }
+
+ private void handleDeleteGroup() {
+ int selectedRow = groupTable.getSelectedRow();
+ if (selectedRow >= 0) {
+ LookupGroup group = groupTableModel.getGroup(selectedRow);
+ int confirm = JOptionPane.showConfirmDialog(parent,
+ "Are you sure you want to delete group: " + group.getName() + "?",
+ "Confirm Delete",
+ JOptionPane.YES_NO_OPTION);
+
+ if (confirm != JOptionPane.YES_OPTION) {
+ return;
+ }
+
+ // remove from server
+ try {
+ LookupServiceClient.getInstance().deleteGroup(group.getId());
+
+ // remove from UI
+ groupTableModel.removeGroup(selectedRow);
+ if (groupTableModel.getRowCount() > 0) {
+ int newIndex = Math.min(selectedRow, groupTableModel.getRowCount() - 1);
+ groupTable.setRowSelectionInterval(newIndex, newIndex);
+ } else {
+ groupTable.clearSelection();
+ }
+ } catch (LookupApiClientException e) {
+ showError(e.getError().getMessage());
+ } catch (Exception e) {
+ logger.error("Unexpected error while remove value", e);
+ showError("Unexpected error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
+ }
+ }
+ }
+
+ private void handleExportJson() {
+ int selectedRow = groupTable.getSelectedRow();
+ if (selectedRow < 0) {
+ return;
+ }
+
+ LookupGroup group = groupTableModel.getGroup(selectedRow);
+
+ String groupNameSlug = group.getName().toLowerCase().replaceAll("[^a-z0-9]+", "_");
+ String dateString = new SimpleDateFormat("yyyy_MM_dd_HH_mm").format(new Date());
+ String defaultFileName = "lookup_group_" + groupNameSlug + "_export_" + dateString + ".json";
+
+ final File file = new FileChooser().createFileForExport(parent, defaultFileName, "json");
+ if (file == null) {
+ return;
+ }
+
+ // Progress UI setup
+ JDialog progressDialog = new JDialog(parent, "Exporting JSON", true);
+ JProgressBar progressBar = new JProgressBar(0, 100);
+ progressBar.setStringPainted(true);
+ JLabel statusLabel = new JLabel("Exported 0 of ? entries");
+
+ JButton cancelButton = new JButton("Cancel");
+
+ JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+ buttonPanel.add(cancelButton);
+
+ progressDialog.setLayout(new BorderLayout(10, 10));
+ progressDialog.add(statusLabel, BorderLayout.NORTH);
+ progressDialog.add(progressBar, BorderLayout.CENTER);
+ progressDialog.add(buttonPanel, BorderLayout.SOUTH);
+ progressDialog.setSize(350, 120);
+ progressDialog.setLocationRelativeTo(parent);
+ progressDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
+
+ SwingWorker exportWorker = new SwingWorker() {
+ @Override
+ protected Void doInBackground() throws Exception {
+ int offset = 0;
+ int limit = 10000;
+ int processed = 0;
+ int total = -1;
+ Date exportDate = null;
+
+ Map allValues = new LinkedHashMap<>();
+
+ try {
+ ExportGroupPagedResponse page = LookupServiceClient.getInstance().exportGroupPaged(group.getId(), offset, limit);
+
+ exportDate = page.getExportDate();
+ total = page.getTotalCount();
+ allValues.putAll(page.getValues());
+ processed += page.getValues().size();
+ publishProgress(processed, total);
+
+ while (!isCancelled() && page.getPagination().isHasMore()) {
+ offset += limit;
+ page = LookupServiceClient.getInstance().exportGroupPaged(group.getId(), offset, limit);
+ allValues.putAll(page.getValues());
+ processed += page.getValues().size();
+ publishProgress(processed, total);
+ }
+
+ ExportLookupGroupResponse exportResponse = new ExportLookupGroupResponse();
+ exportResponse.setGroup(group);
+ exportResponse.setValues(allValues);
+ exportResponse.setExportDate(exportDate);
+
+ String json = JsonUtils.toJsonPretty(exportResponse);
+ java.nio.file.Files.write(file.toPath(), json.getBytes());
+
+ } catch (Exception ex) {
+ logger.error("Failed to export group to JSON", ex);
+ throw ex;
+ }
+
+ return null;
+ }
+
+ private void publishProgress(int processed, int total) {
+ int progress = total > 0 ? (int) ((processed / (double) total) * 100) : 0;
+ progress = Math.min(progress, 100);
+
+ publish(new int[]{progress, processed, total});
+ }
+
+ @Override
+ protected void process(List chunks) {
+ if (!chunks.isEmpty()) {
+ int[] latest = chunks.get(chunks.size() - 1);
+ progressBar.setValue(latest[0]);
+ statusLabel.setText("Exported " + latest[1] + " of " + latest[2] + " entries");
+ }
+ }
+
+ @Override
+ protected void done() {
+ progressDialog.dispose();
+ if (isCancelled()) {
+ JOptionPane.showMessageDialog(parent, "Export cancelled by user.", "Cancelled", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+
+ try {
+ get(); // triggers exception handling if doInBackground failed
+
+ JOptionPane.showMessageDialog(parent, "JSON export completed.", "Export Complete", JOptionPane.INFORMATION_MESSAGE);
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ showError("Export failed: " + (cause.getMessage() != null ? cause.getMessage() : cause.getClass().getSimpleName()));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // restore interrupt status
+ showError("Export was interrupted.");
+ }
+ }
+ };
+
+ cancelButton.addActionListener(e -> exportWorker.cancel(true));
+
+ progressDialog.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ int confirm = JOptionPane.showConfirmDialog(
+ progressDialog,
+ "Export is still in progress. Do you want to cancel?",
+ "Confirm Cancel",
+ JOptionPane.YES_NO_OPTION
+ );
+ if (confirm == JOptionPane.YES_OPTION) {
+ exportWorker.cancel(true);
+ }
+ }
+ });
+
+ exportWorker.execute();
+ progressDialog.setVisible(true);
+ }
+
+ // Public methods for external interaction
+ public void addGroupSelectionListener(ListSelectionListener listener) {
+ groupTable.getSelectionModel().addListSelectionListener(listener);
+ }
+
+ public LookupGroup getSelectedGroup() {
+ int selectedRow = groupTable.getSelectedRow();
+ return selectedRow >= 0 ? groupTableModel.getGroup(selectedRow) : null;
+ }
+
+ public void clearGroups() {
+ groupTableModel.clear();
+ }
+
+ public void updateGroupTable(List groups) {
+ groupTableModel.setGroups(groups);
+ }
+
+ private void showError(String err) {
+ PlatformUI.MIRTH_FRAME.alertError(parent, err);
+ }
+}
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/HistoryPanel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/HistoryPanel.java
new file mode 100644
index 000000000..525dadbfc
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/HistoryPanel.java
@@ -0,0 +1,473 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import com.mirth.connect.client.core.ClientException;
+import com.mirth.connect.client.ui.Frame;
+import com.mirth.connect.client.ui.PlatformUI;
+import com.mirth.connect.client.ui.UIConstants;
+import com.mirth.connect.client.ui.components.MirthDatePicker;
+import com.mirth.connect.client.ui.components.MirthTimePicker;
+
+import com.mirth.connect.model.User;
+
+import com.mirth.connect.plugins.dynamiclookup.client.exception.LookupApiClientException;
+import com.mirth.connect.plugins.dynamiclookup.client.model.LookupAuditTableModel;
+import com.mirth.connect.plugins.dynamiclookup.client.service.LookupServiceClient;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.GroupAuditEntriesResponse;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.HistoryFilterState;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+
+import net.miginfocom.swing.MigLayout;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.swing.JPanel;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.JComboBox;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.UIManager;
+import javax.swing.JScrollPane;
+import javax.swing.SwingWorker;
+import javax.swing.JOptionPane;
+import javax.swing.text.DateFormatter;
+import javax.swing.SwingConstants;
+
+import java.awt.BorderLayout;
+import java.awt.CardLayout;
+import java.awt.FlowLayout;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Objects;
+import java.util.Date;
+import java.util.Calendar;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class HistoryPanel extends JPanel {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ private final Frame parent = PlatformUI.MIRTH_FRAME;
+
+ private JPanel noGroupSelectedPanel;
+ private JPanel contentPanel;
+
+ private JTable auditTable;
+ private LookupAuditTableModel auditTableModel;
+ private JTextField keyFilterField;
+ private JComboBox actionFilterComboBox;
+ private JComboBox userFilterComboBox;
+ private MirthDatePicker startDatePicker;
+ private MirthTimePicker startTimePicker;
+ private MirthDatePicker endDatePicker;
+ private MirthTimePicker endTimePicker;
+
+ private JButton searchButton;
+ private JButton clearButton;
+
+ private JButton prevPageButton;
+ private JButton nextPageButton;
+ private JButton goToPageButton;
+ private JLabel pageInfoLabel;
+ private JComboBox pageSizeComboBox;
+
+ private LookupGroup selectedGroup;
+ private Map userMapById = new LinkedHashMap<>(); //
+
+ private int currentPage = 1;
+ private int pageSize = 25;
+ private int totalCount = 0;
+
+ public HistoryPanel() {
+ initComponents();
+ initLayout();
+
+ updateHistory(null);
+ }
+
+ private void initComponents() {
+ setBackground(UIConstants.BACKGROUND_COLOR);
+
+ // No group selected panel
+ noGroupSelectedPanel = new NoGroupSelectedPanel();
+
+ // Content panel
+ // Filter Group
+ startDatePicker = new MirthDatePicker();
+ startTimePicker = new MirthTimePicker();
+ startTimePicker.setSaveEnabled(false);
+ startDatePicker.addPropertyChangeListener(new PropertyChangeListener() {
+ @Override
+ public void propertyChange(PropertyChangeEvent arg0) {
+ startTimePicker.setEnabled(startDatePicker.getDate() != null);
+ }
+ });
+
+ endDatePicker = new MirthDatePicker();
+ endTimePicker = new MirthTimePicker();
+ endTimePicker.setSaveEnabled(false);
+ endDatePicker.addPropertyChangeListener(new PropertyChangeListener() {
+ @Override
+ public void propertyChange(PropertyChangeEvent arg0) {
+ endTimePicker.setEnabled(endDatePicker.getDate() != null);
+ }
+ });
+
+ keyFilterField = new JTextField();
+ keyFilterField.setToolTipText("Filter History by Key");
+ keyFilterField.addActionListener(e -> {
+ currentPage = 1;
+ loadPage(currentPage);
+ });
+
+ actionFilterComboBox = new JComboBox<>(new String[]{UIConstants.ALL_OPTION, "CREATE", "UPDATE", "DELETE", "DELETE_ALL", "IMPORT", "CLEAR_ALL"});
+ userFilterComboBox = new JComboBox<>(new String[]{}); // Replace with dynamic user list as needed
+
+ // Search button
+ searchButton = new JButton("", UIManager.getIcon("FileView.fileIcon"));
+ searchButton.setIcon(UIConstants.ICON_FILE_PICKER);
+ searchButton.setToolTipText("Search");
+ searchButton.setIconTextGap(5);
+ searchButton.addActionListener(e -> {
+ currentPage = 1;
+ loadPage(currentPage);
+ });
+
+ // Clear Filter then Search button
+ clearButton = new JButton("");
+ clearButton.setIcon(UIConstants.ICON_X);
+ clearButton.setToolTipText("Clear");
+ clearButton.addActionListener(e -> {
+ // Clear all filter fields
+ clearFilterFields();
+
+ currentPage = 1;
+ loadPage(currentPage);
+ });
+
+ // Audit/History Table
+ auditTableModel = new LookupAuditTableModel();
+ auditTable = new JTable(auditTableModel);
+ auditTable.setRowHeight(26);
+
+ prevPageButton = new JButton("Previous");
+ prevPageButton.addActionListener(e -> goToPage(currentPage - 1));
+
+ nextPageButton = new JButton("Next");
+ nextPageButton.addActionListener(e -> goToPage(currentPage + 1));
+
+ goToPageButton = new JButton("Go");
+ goToPageButton.addActionListener(e -> showGoToPageDialog());
+
+ pageInfoLabel = new JLabel("Page 1");
+
+ pageSizeComboBox = new JComboBox<>(new Integer[]{10, 25, 50, 100, 200, 500, 1000});
+ pageSizeComboBox.setSelectedItem(pageSize);
+ pageSizeComboBox.addActionListener(e -> {
+ int selected = (int) pageSizeComboBox.getSelectedItem();
+ if (selected != pageSize) {
+ currentPage = 1;
+ pageSize = selected;
+ loadPage(currentPage);
+ }
+ });
+ }
+
+ private void initLayout() {
+ setLayout(new CardLayout());
+
+ // Content panel with titled border
+ contentPanel = new JPanel(new MigLayout("insets 8, wrap, fill"));
+ contentPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+
+ JPanel topFilterPanel = new JPanel(new MigLayout("insets 0, fillx, wrap"));
+ topFilterPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+
+ // Time Range Group
+ JPanel timeGroup = new JPanel(new MigLayout("insets 0", "[60!][120!]10[100!]", ""));
+ timeGroup.setBackground(UIConstants.BACKGROUND_COLOR);
+
+ JLabel startLabel = new JLabel("Start Time:");
+ startLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+ timeGroup.add(startLabel, "gapright 5");
+ timeGroup.add(startDatePicker, "w 120!, gapright 10");
+ timeGroup.add(startTimePicker, "w 100!, wrap");
+
+ JLabel endLabel = new JLabel("End Time:");
+ endLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+ timeGroup.add(endLabel, "gapright 5");
+ timeGroup.add(endDatePicker, "w 120!, gapright 10");
+ timeGroup.add(endTimePicker, "w 100!");
+ topFilterPanel.add(timeGroup, "growx, wrap");
+
+ // Filter Criteria Group
+ JPanel filterGroup = new JPanel(new MigLayout("insets 0", "[60!][230!]20[][80!]20[][80!]", ""));
+ filterGroup.setBackground(UIConstants.BACKGROUND_COLOR);
+ filterGroup.add(new JLabel("Key:"), "align left, gapright 5");
+ filterGroup.add(keyFilterField, "w 230!");
+ filterGroup.add(new JLabel("Action:"), "align left");
+ filterGroup.add(actionFilterComboBox);
+ filterGroup.add(new JLabel("User:"), "align left");
+ filterGroup.add(userFilterComboBox);
+ topFilterPanel.add(filterGroup, "growx, wrap");
+
+ // Action Buttons Group
+ JPanel actionGroup = new JPanel(new MigLayout("insets 0", "[60!][]10[]", ""));
+ actionGroup.setBackground(UIConstants.BACKGROUND_COLOR);
+ actionGroup.add(new JLabel("Search:"), "align left, gapright 5");
+ actionGroup.add(searchButton);
+ actionGroup.add(clearButton);
+ topFilterPanel.add(actionGroup, "growx, wrap");
+
+ contentPanel.add(topFilterPanel, "growx, wrap");
+ contentPanel.add(new JScrollPane(auditTable), "grow, push, wrap");
+
+ JPanel paginationPanel = new JPanel(new BorderLayout());
+ paginationPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+
+ JPanel leftBottomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ leftBottomPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ leftBottomPanel.add(new JLabel("Page size:"));
+ leftBottomPanel.add(pageSizeComboBox);
+
+ JPanel rightBottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+ rightBottomPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ rightBottomPanel.add(prevPageButton);
+ rightBottomPanel.add(pageInfoLabel);
+ rightBottomPanel.add(nextPageButton);
+ rightBottomPanel.add(goToPageButton);
+
+ paginationPanel.add(leftBottomPanel, BorderLayout.WEST);
+ paginationPanel.add(rightBottomPanel, BorderLayout.EAST);
+
+ contentPanel.add(paginationPanel, "growx, wrap");
+
+ add(contentPanel, "content");
+ add(noGroupSelectedPanel, "noGroup");
+ }
+
+ public void updateHistory(LookupGroup selectedGroup) {
+ boolean showContent = selectedGroup != null;
+
+ contentPanel.setVisible(showContent);
+ noGroupSelectedPanel.setVisible(!showContent);
+
+ // Detect group change
+ if (!Objects.equals(this.selectedGroup, selectedGroup)) {
+ clearFilterFields(); // Clear filters only on group change
+ }
+
+ this.selectedGroup = selectedGroup;
+ this.currentPage = 1;
+
+ loadPage(currentPage);
+ }
+
+ private void loadPage(int page) {
+ auditTableModel.clear();
+
+ if (selectedGroup == null) {
+ return;
+ }
+
+ int offset = (page - 1) * pageSize;
+
+ new SwingWorker() {
+ protected GroupAuditEntriesResponse doInBackground() throws Exception {
+ HistoryFilterState filter = buildHistoryFilterStateFromUI();
+ return LookupServiceClient.getInstance().searchAuditEntries(selectedGroup.getId(), offset, pageSize, filter);
+ }
+
+ protected void done() {
+ try {
+ GroupAuditEntriesResponse response = get();
+ List values = response.getEntries();
+ totalCount = response.getTotalEntries();
+ auditTableModel.setValues(values);
+ currentPage = page;
+ updatePaginationControls();
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof LookupApiClientException) {
+ showError("Failed to load audit entries: " + cause.getMessage());
+ } else {
+ logger.error("Unexpected error while loading audit entries", e);
+ showError("An unexpected error occurred: " + cause.getMessage());
+ }
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt(); // Restore interrupt status
+ showError("Operation was interrupted.");
+ }
+ }
+ }.execute();
+ }
+
+ private void updatePaginationControls() {
+ int totalPages = (int) Math.ceil((double) totalCount / pageSize);
+ pageInfoLabel.setText("Page " + currentPage + " of " + totalPages);
+ prevPageButton.setEnabled(currentPage > 1);
+ nextPageButton.setEnabled(currentPage < totalPages);
+ }
+
+ private void goToPage(int page) {
+ if (page >= 1 && (page - 1) * pageSize < totalCount) {
+ loadPage(page);
+ }
+ }
+
+ private void showGoToPageDialog() {
+ if (totalCount == 0) {
+ return;
+ }
+
+ JComboBox pageSelector = new JComboBox<>();
+ int totalPages = (int) Math.ceil((double) totalCount / pageSize);
+
+ for (int i = 1; i <= totalPages; i++) {
+ pageSelector.addItem(i);
+ }
+
+ int result = JOptionPane.showConfirmDialog(this, pageSelector, "Select Page", JOptionPane.OK_CANCEL_OPTION);
+ if (result == JOptionPane.OK_OPTION) {
+ Integer selectedPage = (Integer) pageSelector.getSelectedItem();
+ if (selectedPage != null) {
+ loadPage(selectedPage);
+ }
+ }
+ }
+
+ public void updateCachedUserMap() {
+ try {
+ // Retrieve updated user list from server
+ parent.retrieveUsers(); // populates parent.users
+ } catch (ClientException e) {
+ parent.alertThrowable(this, e);
+ return;
+ }
+
+ // Clear and repopulate the ID → name map
+ userMapById.clear();
+ userMapById.put(-1, UIConstants.ALL_OPTION); // default: "All Users"
+ userMapById.put(0, "System"); // system user
+
+ for (User user : parent.users) {
+ userMapById.put(user.getId(), user.getUsername());
+ }
+
+ // Now update the combo box with usernames
+ userFilterComboBox.removeAllItems();
+ for (String username : userMapById.values()) {
+ userFilterComboBox.addItem(username);
+ }
+
+ userFilterComboBox.setSelectedIndex(0);
+ }
+
+ private String getSelectedUserId() {
+ String selectedName = (String) userFilterComboBox.getSelectedItem();
+
+ if (selectedName == null || selectedName.equals(UIConstants.ALL_OPTION)) {
+ return null;
+ }
+
+ for (Map.Entry entry : userMapById.entrySet()) {
+ if (entry.getValue().equals(selectedName)) {
+ return String.valueOf(entry.getKey()); // convert int ID to String
+ }
+ }
+
+ return null;
+ }
+
+ private void clearFilterFields() {
+ keyFilterField.setText("");
+ actionFilterComboBox.setSelectedIndex(0);
+ userFilterComboBox.setSelectedIndex(0);
+ startDatePicker.setDate(null);
+ endDatePicker.setDate(null);
+ }
+
+ private HistoryFilterState buildHistoryFilterStateFromUI() {
+ String keyValue = normalizeField(keyFilterField.getText());
+ String action = normalizeComboBoxValue((String) actionFilterComboBox.getSelectedItem());
+ String userId = getSelectedUserId();
+
+ Date startDateTime = getCombinedDateTime(startDatePicker, startTimePicker);
+ Date endDateTime = getCombinedDateTime(endDatePicker, endTimePicker);
+
+ HistoryFilterState filter = new HistoryFilterState();
+ filter.setKeyValue(keyValue);
+ filter.setAction(action);
+ filter.setUserId(userId);
+ filter.setStartDate(startDateTime);
+ filter.setEndDate(endDateTime);
+
+ return filter;
+ }
+
+ private String normalizeField(String value) {
+ return (value == null || value.trim().isEmpty()) ? null : value.trim();
+ }
+
+ private String normalizeComboBoxValue(String value) {
+ return (value == null || UIConstants.ALL_OPTION.equals(value.trim())) ? null : value.trim();
+ }
+
+ private Date getCombinedDateTime(MirthDatePicker datePicker, MirthTimePicker timePicker) {
+ try {
+ Date date = datePicker.getDate();
+ String time = timePicker.getDate(); // returns formatted time string like "10:15 AM"
+
+ if (date != null && time != null) {
+ DateFormatter timeFormatter = new DateFormatter(new SimpleDateFormat("hh:mm aa"));
+ Date parsedTime = (Date) timeFormatter.stringToValue(time);
+
+ Calendar dateCal = Calendar.getInstance();
+ dateCal.setTime(date);
+
+ Calendar timeCal = Calendar.getInstance();
+ timeCal.setTime(parsedTime);
+
+ Calendar combinedCal = Calendar.getInstance();
+ combinedCal.setTime(date);
+ if (timePicker.isEnabled()) {
+ combinedCal.set(Calendar.HOUR_OF_DAY, timeCal.get(Calendar.HOUR_OF_DAY));
+ combinedCal.set(Calendar.MINUTE, timeCal.get(Calendar.MINUTE));
+ combinedCal.set(Calendar.SECOND, timeCal.get(Calendar.SECOND));
+ combinedCal.set(Calendar.MILLISECOND, 0);
+ } else {
+ combinedCal.set(Calendar.HOUR_OF_DAY, 0);
+ combinedCal.set(Calendar.MINUTE, 0);
+ combinedCal.set(Calendar.SECOND, 0);
+ combinedCal.set(Calendar.MILLISECOND, 0);
+ }
+
+ return combinedCal.getTime();
+ }
+ } catch (ParseException e) {
+ // Optionally log or ignore
+ }
+
+ return null;
+ }
+
+ private void showError(String err) {
+ PlatformUI.MIRTH_FRAME.alertError(parent, err);
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/LookupTablePanel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/LookupTablePanel.java
new file mode 100644
index 000000000..784b43025
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/LookupTablePanel.java
@@ -0,0 +1,160 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import com.mirth.connect.client.core.ClientException;
+import com.mirth.connect.client.ui.Frame;
+import com.mirth.connect.client.ui.PlatformUI;
+
+import com.mirth.connect.plugins.dynamiclookup.client.service.LookupServiceClient;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+
+import net.miginfocom.swing.MigLayout;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+import javax.swing.JSplitPane;
+import javax.swing.SwingWorker;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * @author Thai Tran (thaitran@innovarhealthcare.com)
+ * @create 2025-05-13 10:25 AM
+ */
+public class LookupTablePanel extends JPanel {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ private final DataStorePanel taskPane;
+ private final GroupPanel groupPanel;
+ private final DetailsPanel detailsPanel;
+ private final ValuePanel valuePanel;
+ private final CacheStatusPanel cachePanel;
+ private final HistoryPanel historyPanel;
+ private final JTabbedPane tabbedPane;
+
+ private final Frame parent = PlatformUI.MIRTH_FRAME;
+
+ public LookupTablePanel(DataStorePanel taskPane) {
+ this.taskPane = taskPane;
+
+ this.groupPanel = new GroupPanel();
+ this.detailsPanel = new DetailsPanel();
+ this.valuePanel = new ValuePanel();
+ this.cachePanel = new CacheStatusPanel();
+ this.historyPanel = new HistoryPanel();
+ this.tabbedPane = new JTabbedPane();
+
+ initComponents();
+ initLayout();
+ }
+
+ private void initComponents() {
+ // Connect group selection to panel update based on selected tab
+ groupPanel.addGroupSelectionListener(e -> {
+ LookupGroup selectedGroup = groupPanel.getSelectedGroup();
+ int selectedTab = tabbedPane.getSelectedIndex();
+ switch (selectedTab) {
+ case 0: // Values tab
+ detailsPanel.updateDetails(selectedGroup);
+ break;
+ case 1: // Values tab
+ valuePanel.updateValues(selectedGroup);
+ break;
+ case 2: // Cache tab
+ cachePanel.updateCaches(selectedGroup);
+ break;
+ case 3: // History tab
+ historyPanel.updateHistory(selectedGroup);
+ break;
+ }
+ });
+
+ // Tab change listener to trigger update when user switches tab
+ tabbedPane.addChangeListener(e -> {
+ LookupGroup selectedGroup = groupPanel.getSelectedGroup();
+ int selectedTab = tabbedPane.getSelectedIndex();
+ switch (selectedTab) {
+ case 0:
+ detailsPanel.updateDetails(selectedGroup);
+ break;
+ case 1:
+ valuePanel.updateValues(selectedGroup);
+ break;
+ case 2:
+ cachePanel.updateCaches(selectedGroup);
+ break;
+ case 3:
+ historyPanel.updateCachedUserMap(); // this will call retrieveUsers()
+ historyPanel.updateHistory(selectedGroup);
+ break;
+ }
+ });
+ }
+
+ private void initLayout() {
+ setLayout(new MigLayout("insets 0, novisualpadding, hidemode 3, fill"));
+
+ // Setup tabbed pane with panels
+ tabbedPane.addTab("Details", detailsPanel);
+ tabbedPane.addTab("Values", valuePanel);
+ tabbedPane.addTab("Cache Status", cachePanel);
+ tabbedPane.addTab("History", historyPanel);
+
+ // Split Pane for Group and Value Panels
+ JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
+ splitPane.setDividerLocation(300);
+ splitPane.setResizeWeight(0.5);
+ splitPane.setLeftComponent(groupPanel);
+ splitPane.setRightComponent(tabbedPane);
+
+ add(splitPane, "grow, push");
+ }
+
+ public void doRefresh() {
+ final String workingId = parent.startWorking("Loading lookup groups...");
+
+ SwingWorker, Void> worker = new SwingWorker, Void>() {
+
+ @Override
+ public List doInBackground() throws ClientException {
+ return LookupServiceClient.getInstance().getAllGroups();
+ }
+
+ @Override
+ public void done() {
+ try {
+ groupPanel.updateGroupTable(get());
+ } catch (Throwable t) {
+ if (t instanceof ExecutionException) {
+ t = t.getCause();
+ }
+ parent.alertThrowable(parent, t, "Error loading groups: " + t.toString());
+ } finally {
+ parent.stopWorking(workingId);
+ }
+ }
+ };
+
+ worker.execute();
+ }
+
+ @Override
+ public void removeNotify() {
+ super.removeNotify();
+
+ taskPane.unBold();
+ }
+}
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/NoGroupSelectedPanel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/NoGroupSelectedPanel.java
new file mode 100644
index 000000000..6e32b0d13
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/NoGroupSelectedPanel.java
@@ -0,0 +1,39 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import com.mirth.connect.client.ui.UIConstants;
+
+import javax.swing.JPanel;
+import javax.swing.JLabel;
+import javax.swing.UIManager;
+import javax.swing.SwingConstants;
+
+import java.awt.BorderLayout;
+import java.awt.Font;
+
+public class NoGroupSelectedPanel extends JPanel {
+ public NoGroupSelectedPanel() {
+ super(new BorderLayout());
+ setBackground(UIConstants.BACKGROUND_COLOR);
+
+ JLabel messageLabel = new JLabel(
+ "No group selected. Please choose one to proceed.",
+ UIManager.getIcon("OptionPane.warningIcon"),
+ SwingConstants.CENTER
+ );
+ messageLabel.setFont(messageLabel.getFont().deriveFont(Font.BOLD, 16f));
+ messageLabel.setHorizontalTextPosition(SwingConstants.RIGHT);
+ messageLabel.setIconTextGap(10);
+
+ add(messageLabel, BorderLayout.CENTER);
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ValuePanel.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ValuePanel.java
new file mode 100644
index 000000000..7b86e772f
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/panel/ValuePanel.java
@@ -0,0 +1,775 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.panel;
+
+import com.mirth.connect.client.ui.Frame;
+import com.mirth.connect.client.ui.PlatformUI;
+import com.mirth.connect.client.ui.UIConstants;
+
+import com.mirth.connect.plugins.dynamiclookup.client.dialog.LookupValueDialog;
+import com.mirth.connect.plugins.dynamiclookup.client.exception.LookupApiClientException;
+import com.mirth.connect.plugins.dynamiclookup.client.model.LookupValueTableModel;
+import com.mirth.connect.plugins.dynamiclookup.client.service.LookupServiceClient;
+import com.mirth.connect.plugins.dynamiclookup.client.util.CsvLineParser;
+import com.mirth.connect.plugins.dynamiclookup.client.util.FileChooser;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.ImportValuesResponse;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.LookupAllValuesResponse;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+
+import net.miginfocom.swing.MigLayout;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.swing.JPanel;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JComboBox;
+import javax.swing.UIManager;
+import javax.swing.JScrollPane;
+import javax.swing.SwingWorker;
+import javax.swing.JOptionPane;
+import javax.swing.JFileChooser;
+import javax.swing.JDialog;
+import javax.swing.JProgressBar;
+import javax.swing.SwingUtilities;
+import javax.swing.filechooser.FileNameExtensionFilter;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.CardLayout;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+import java.io.File;
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.SimpleDateFormat;
+
+import java.util.Objects;
+import java.util.List;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Date;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * @author Thai Tran (thaitran@innovarhealthcare.com)
+ * @create 2025-05-13 10:25 AM
+ */
+public class ValuePanel extends JPanel {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ private final Frame parent = PlatformUI.MIRTH_FRAME;
+
+ private JPanel noGroupSelectedPanel;
+ private JPanel contentPanel;
+
+ private JTable valueTable;
+ private LookupValueTableModel valueTableModel;
+ private JTextField valueFilterField;
+ private JButton searchButton;
+ private JButton addValueButton;
+ private JButton importCsvButton;
+ private JButton exportButton;
+ private JButton prevPageButton;
+ private JButton nextPageButton;
+ private JButton goToPageButton;
+ private JLabel pageInfoLabel;
+ private JComboBox pageSizeComboBox;
+
+ private LookupGroup selectedGroup;
+
+ private int currentPage = 1;
+ private int pageSize = 25;
+ private int totalCount = 0;
+
+ public ValuePanel() {
+ initComponents();
+ initLayout();
+
+ updateValues(null);
+ }
+
+ private void initComponents() {
+ setBackground(UIConstants.BACKGROUND_COLOR);
+
+ // No group selected panel
+ noGroupSelectedPanel = new NoGroupSelectedPanel();
+
+ // Content panel
+
+ // Value Filter
+ valueFilterField = new JTextField();
+ valueFilterField.setToolTipText("Filter values by key or value");
+ valueFilterField.addActionListener(e -> {
+ currentPage = 1;
+ loadPage(currentPage);
+ });
+
+ // Value Table
+ valueTableModel = new LookupValueTableModel();
+ valueTable = new JTable(valueTableModel);
+ valueTable.setRowHeight(26);
+ valueTable.getColumnModel().getColumn(LookupValueTableModel.ACTION_COLUMN).setCellRenderer(new ButtonRenderer());
+ valueTable.getColumnModel().getColumn(LookupValueTableModel.ACTION_COLUMN).setCellEditor(new ButtonEditor(valueTable, valueTableModel,
+ e -> handleEditValue((Integer) e.getSource()),
+ e -> handleRemoveValue((Integer) e.getSource())));
+ valueTable.getColumnModel().getColumn(LookupValueTableModel.ACTION_COLUMN).setPreferredWidth(120); // Adjust for button width
+
+ // Search/Filter button
+ searchButton = new JButton("Search", UIManager.getIcon("FileView.fileIcon"));
+ searchButton.setIcon(UIConstants.ICON_FILE_PICKER);
+ searchButton.setToolTipText("Search");
+ searchButton.setIconTextGap(5);
+ searchButton.addActionListener(e -> {
+ currentPage = 1;
+ loadPage(currentPage);
+ });
+
+ // Value Buttons
+ addValueButton = new JButton("Add");
+ addValueButton.addActionListener(e -> {
+ handleAddValue();
+ });
+
+ importCsvButton = new JButton("Import Csv");
+ importCsvButton.addActionListener(e -> {
+ handleImportCsv();
+ });
+
+ exportButton = new JButton("Export");
+ exportButton.addActionListener(e -> {
+ handleExport();
+ });
+
+ prevPageButton = new JButton("Previous");
+ prevPageButton.addActionListener(e -> goToPage(currentPage - 1));
+
+ nextPageButton = new JButton("Next");
+ nextPageButton.addActionListener(e -> goToPage(currentPage + 1));
+
+ goToPageButton = new JButton("Go");
+ goToPageButton.addActionListener(e -> showGoToPageDialog());
+
+ pageInfoLabel = new JLabel("Page 1");
+
+ pageSizeComboBox = new JComboBox<>(new Integer[]{10, 25, 50, 100, 200, 500, 1000});
+ pageSizeComboBox.setSelectedItem(pageSize);
+ pageSizeComboBox.addActionListener(e -> {
+ int selected = (int) pageSizeComboBox.getSelectedItem();
+ if (selected != pageSize) {
+ currentPage = 1;
+ pageSize = selected;
+ loadPage(currentPage);
+ }
+ });
+ }
+
+ private void initLayout() {
+ setLayout(new CardLayout());
+
+ // Content panel with titled border
+ contentPanel = new JPanel(new MigLayout("insets 8, fill"));
+ contentPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+
+ JPanel topButtonPanel = new JPanel(new MigLayout("insets 0, fill", "", ""));
+ topButtonPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ topButtonPanel.add(valueFilterField, "w 150!, growx, split 2");
+ topButtonPanel.add(searchButton);
+ topButtonPanel.add(addValueButton, "gapleft push, split 3");
+ topButtonPanel.add(importCsvButton);
+ topButtonPanel.add(exportButton);
+
+ contentPanel.add(topButtonPanel, "growx, wrap");
+ contentPanel.add(new JScrollPane(valueTable), "grow, push, wrap");
+
+ JPanel paginationPanel = new JPanel(new BorderLayout());
+ paginationPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+
+ JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ leftPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ leftPanel.add(new JLabel("Page size:"));
+ leftPanel.add(pageSizeComboBox);
+
+ JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+ rightPanel.setBackground(UIConstants.BACKGROUND_COLOR);
+ rightPanel.add(prevPageButton);
+ rightPanel.add(pageInfoLabel);
+ rightPanel.add(nextPageButton);
+ rightPanel.add(goToPageButton);
+
+ paginationPanel.add(leftPanel, BorderLayout.WEST);
+ paginationPanel.add(rightPanel, BorderLayout.EAST);
+
+ contentPanel.add(paginationPanel, "growx, wrap");
+
+ add(contentPanel, "content");
+ add(noGroupSelectedPanel, "noGroup");
+ }
+
+ public void updateValues(LookupGroup selectedGroup) {
+ boolean showContent = selectedGroup != null;
+
+ contentPanel.setVisible(showContent);
+ noGroupSelectedPanel.setVisible(!showContent);
+
+ // Detect group change
+ if (!Objects.equals(this.selectedGroup, selectedGroup)) {
+ // Clear filters only on group change
+ valueFilterField.setText("");
+ }
+
+ this.selectedGroup = selectedGroup;
+ this.currentPage = 1;
+
+ loadPage(currentPage);
+ }
+
+ private void loadPage(int page) {
+ valueTableModel.clear();
+
+ if (selectedGroup == null) {
+ return;
+ }
+
+ int offset = (page - 1) * pageSize;
+
+ new SwingWorker() {
+ protected LookupAllValuesResponse doInBackground() throws Exception {
+ String pattern = valueFilterField.getText().trim();
+ return LookupServiceClient.getInstance().getAllValues(selectedGroup.getId(), offset, pageSize, pattern);
+ }
+
+ protected void done() {
+ try {
+ LookupAllValuesResponse response = get();
+ List values = response.getValues();
+ totalCount = response.getTotalCount();
+ valueTableModel.setValues(values);
+ currentPage = page;
+ updatePaginationControls();
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof LookupApiClientException) {
+ showError("Failed to load values: " + cause.getMessage());
+ } else {
+ logger.error("Unexpected error while loading values", e);
+ showError("An unexpected error occurred: " + cause.getMessage());
+ }
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt(); // Restore interrupt status
+ showError("Operation was interrupted.");
+ }
+ }
+ }.execute();
+ }
+
+ private void updatePaginationControls() {
+ int totalPages = (int) Math.ceil((double) totalCount / pageSize);
+ pageInfoLabel.setText("Page " + currentPage + " of " + totalPages);
+ prevPageButton.setEnabled(currentPage > 1);
+ nextPageButton.setEnabled(currentPage < totalPages);
+ }
+
+ private void goToPage(int page) {
+ if (page >= 1 && (page - 1) * pageSize < totalCount) {
+ loadPage(page);
+ }
+ }
+
+ private void showGoToPageDialog() {
+ if (totalCount == 0) {
+ return;
+ }
+
+ JComboBox pageSelector = new JComboBox<>();
+ int totalPages = (int) Math.ceil((double) totalCount / pageSize);
+
+ for (int i = 1; i <= totalPages; i++) {
+ pageSelector.addItem(i);
+ }
+
+ int result = JOptionPane.showConfirmDialog(this, pageSelector, "Select Page", JOptionPane.OK_CANCEL_OPTION);
+ if (result == JOptionPane.OK_OPTION) {
+ Integer selectedPage = (Integer) pageSelector.getSelectedItem();
+ if (selectedPage != null) {
+ loadPage(selectedPage);
+ }
+ }
+ }
+
+ private void setPaginationControlsEnabled(boolean enabled) {
+ prevPageButton.setEnabled(enabled);
+ nextPageButton.setEnabled(enabled);
+ pageSizeComboBox.setEnabled(enabled);
+ }
+
+ private void handleAddValue() {
+ if (selectedGroup != null) {
+ LookupValue lookupValue = new LookupValue();
+ LookupValueDialog dialog = new LookupValueDialog(parent, lookupValue, selectedGroup, false);
+ if (dialog.isSaved()) {
+ currentPage = 1;
+ loadPage(currentPage);
+ }
+ } else {
+ showError("Please select a Group");
+ }
+ }
+
+ private void handleEditValue(int row) {
+ if (row >= 0) {
+ LookupValue lookupValue = valueTableModel.getValue(row);
+ LookupValue copy = new LookupValue(lookupValue);
+ if (selectedGroup != null) {
+ LookupValueDialog dialog = new LookupValueDialog(parent, copy, selectedGroup, true);
+ if (dialog.isSaved()) {
+ loadPage(currentPage);
+ }
+ }
+ }
+ }
+
+ private void handleRemoveValue(int row) {
+ LookupValue value = valueTableModel.getValue(row);
+ if (row >= 0) {
+ int confirm = JOptionPane.showConfirmDialog(parent,
+ "Are you sure you want to delete value with key: " + value.getKeyValue() + "?",
+ "Confirm Delete",
+ JOptionPane.YES_NO_OPTION);
+
+ if (confirm == JOptionPane.YES_OPTION) {
+ if (selectedGroup != null) {
+ try {
+ LookupServiceClient.getInstance().deleteValue(selectedGroup.getId(), value.getKeyValue());
+ loadPage(currentPage);
+ } catch (LookupApiClientException e) {
+ showError(e.getError().getMessage());
+ } catch (Exception e) {
+ logger.error("Unexpected error while remove value", e);
+ showError("Unexpected error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
+ }
+ }
+ }
+ }
+ }
+
+ private void handleImportCsv() {
+ if (selectedGroup == null) {
+ showError("Please select a Group");
+ return;
+ }
+
+ JFileChooser importFileChooser = new JFileChooser();
+ importFileChooser.setFileFilter(new FileNameExtensionFilter("CSV files", "csv"));
+
+ File currentDir = new File(Frame.userPreferences.get("currentDirectory", ""));
+ if (currentDir.exists()) {
+ importFileChooser.setCurrentDirectory(currentDir);
+ }
+
+ if (importFileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
+ File file = importFileChooser.getSelectedFile();
+ if (file != null) {
+
+ if (!checkCsvFile(file)) {
+ return;
+ }
+
+ int result = JOptionPane.showConfirmDialog(
+ this,
+ "Do you want to clear existing values before import?",
+ "Clear Existing Values?",
+ JOptionPane.YES_NO_OPTION
+ );
+
+ boolean clearExisting = (result == JOptionPane.YES_OPTION);
+
+ // Create modal progress dialog
+ JDialog progressDialog = new JDialog(this.parent, "Importing CSV", true);
+ JProgressBar progressBar = new JProgressBar(0, 100);
+ progressBar.setStringPainted(true);
+ JLabel statusLabel = new JLabel("Imported 0 of ? entries");
+
+ JButton cancelButton = new JButton("Cancel");
+ JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+ buttonPanel.add(cancelButton);
+
+ progressDialog.setLayout(new BorderLayout(10, 10));
+ progressDialog.add(statusLabel, BorderLayout.NORTH);
+ progressDialog.add(progressBar, BorderLayout.CENTER);
+ progressDialog.add(buttonPanel, BorderLayout.SOUTH);
+ progressDialog.setSize(350, 120);
+ progressDialog.setLocationRelativeTo(this.parent);
+ progressDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
+
+ SwingWorker importWorker = new SwingWorker() {
+ @Override
+ protected Void doInBackground() throws Exception {
+ try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+ // Count total lines for accurate progress
+ int totalLines = -1; // skip header
+ while (reader.readLine() != null) {
+ totalLines++;
+ }
+
+ // Re-open reader for actual import
+ try (BufferedReader reader2 = new BufferedReader(new FileReader(file))) {
+ String line;
+ boolean isFirstLine = true;
+ boolean isFirstBatch = true;
+ int lineNumber = 0;
+ int processedLines = 0;
+ int batchSize = 100;
+ Map batchMap = new LinkedHashMap<>();
+
+ while ((line = reader2.readLine()) != null && !isCancelled()) {
+ lineNumber++;
+ if (isFirstLine) {
+ isFirstLine = false;
+ continue;
+ }
+
+ if (line.trim().isEmpty()) continue;
+
+ Map.Entry entry = CsvLineParser.parseLine(line, lineNumber);
+ if (entry == null) {
+ logger.warn("Skipping malformed or empty entry at line " + lineNumber + ": " + line);
+ continue;
+ }
+
+ batchMap.put(entry.getKey(), entry.getValue());
+
+ processedLines++;
+ publishProgress(processedLines, totalLines);
+
+ if (batchMap.size() == batchSize) {
+ importValues(selectedGroup.getId(), batchMap, clearExisting && isFirstBatch);
+ batchMap.clear();
+ isFirstBatch = false;
+ }
+ }
+
+ if (!isCancelled() && !batchMap.isEmpty()) {
+ importValues(selectedGroup.getId(), batchMap, clearExisting && isFirstBatch);
+ }
+
+ Frame.userPreferences.put("currentDirectory", file.getParent());
+ }
+
+ } catch (Exception e) {
+ logger.error("Failed to import lookup values from CSV file", e);
+ throw e;
+ }
+
+ return null;
+ }
+
+ private void publishProgress(int processed, int total) {
+ int progress = total > 0 ? (int) ((processed / (double) total) * 100) : 0;
+ progress = Math.min(progress, 100);
+
+ publish(new int[]{progress, processed, total});
+ }
+
+ @Override
+ protected void process(List chunks) {
+ if (!chunks.isEmpty()) {
+ int[] latest = chunks.get(chunks.size() - 1);
+ int progress = latest[0];
+ int imported = latest[1];
+ int total = latest[2];
+ progressBar.setValue(progress);
+ statusLabel.setText("Imported " + imported + " of " + total + " entries");
+ }
+ }
+
+ @Override
+ protected void done() {
+ progressDialog.dispose();
+
+ updateValues(selectedGroup);
+
+ if (isCancelled()) {
+ JOptionPane.showMessageDialog(parent, "CSV import was cancelled.", "Import Cancelled", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+
+ try {
+ get(); // triggers exception handling if doInBackground failed
+
+ JOptionPane.showMessageDialog(parent, "CSV import completed.", "Import Complete", JOptionPane.INFORMATION_MESSAGE);
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ showError("Import failed: " + (cause.getMessage() != null ? cause.getMessage() : cause.getClass().getSimpleName()));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // restore interrupt status
+ showError("Import was interrupted.");
+ }
+ }
+ };
+
+ // Cancel button
+ cancelButton.addActionListener(e -> importWorker.cancel(true));
+
+ // Window close
+ progressDialog.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ int confirm = JOptionPane.showConfirmDialog(
+ progressDialog,
+ "Import is still in progress. Do you want to cancel?",
+ "Confirm Cancel",
+ JOptionPane.YES_NO_OPTION
+ );
+ if (confirm == JOptionPane.YES_OPTION) {
+ importWorker.cancel(true);
+ }
+ }
+ });
+
+ importWorker.execute();
+ progressDialog.setVisible(true);
+ }
+ }
+ }
+
+ private boolean checkCsvFile(File file) {
+ // 1. Extension check
+ if (!file.getName().toLowerCase().endsWith(".csv")) {
+ showError("File does not have a .csv extension.");
+ return false;
+ }
+
+ // 2. MIME type check
+ try {
+ Path path = file.toPath();
+ String mimeType = Files.probeContentType(path);
+ if (mimeType == null || !(
+ mimeType.equals("text/plain") ||
+ mimeType.equals("text/csv") ||
+ mimeType.equals("application/vnd.ms-excel") ||
+ mimeType.startsWith("text/")
+ )) {
+ showError("File does not appear to be a valid CSV (detected type: " + mimeType + ").");
+ return false;
+ }
+ } catch (IOException e) {
+ showError("Failed to detect file type: " + e.getMessage());
+ return false;
+ }
+
+ // 3. Content check
+ try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+ String header = reader.readLine();
+ if (header == null || header.split(",", -1).length < 2) {
+ showError("CSV file must contain at least two columns: key and value.");
+ return false;
+ }
+ } catch (IOException e) {
+ showError("Failed to read file for content validation: " + e.getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ private void importValues(Integer groupId, Map values, boolean clearExisting) {
+ try {
+ ImportValuesResponse response = LookupServiceClient.getInstance().importValues(groupId, clearExisting, values);
+ } catch (LookupApiClientException e) {
+ showError(e.getError().getMessage());
+ } catch (Exception e) {
+ logger.error("Unexpected error while importing values", e);
+ showError("Unexpected error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
+ }
+ }
+
+ private void handleExport() {
+ if (selectedGroup == null) {
+ showError("Please select a Group");
+ return;
+ }
+
+ Date currentDate = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy_MM_dd_HH_mm");
+ String dateString = formatter.format(currentDate);
+ String defaultFileName = "all_values_" + dateString + ".csv";
+
+ final File file = new FileChooser().createFileForExport(parent, defaultFileName, "csv");
+ if (file == null) {
+ return;
+ }
+
+ // Create modal dialog
+ JDialog progressDialog = new JDialog(this.parent, "Exporting CSV", true); // modal = true
+ JProgressBar progressBar = new JProgressBar(0, 100);
+ progressBar.setStringPainted(true);
+ JLabel statusLabel = new JLabel("Exported 0 of ? entries");
+
+ JButton cancelButton = new JButton("Cancel");
+
+ JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+ buttonPanel.add(cancelButton);
+
+ progressDialog.setLayout(new BorderLayout(10, 10));
+ progressDialog.add(statusLabel, BorderLayout.NORTH);
+ progressDialog.add(progressBar, BorderLayout.CENTER);
+ progressDialog.add(buttonPanel, BorderLayout.SOUTH);
+ progressDialog.setSize(350, 120);
+ progressDialog.setLocationRelativeTo(this.parent);
+ progressDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
+
+ SwingWorker exportWorker = new SwingWorker() {
+ @Override
+ protected Void doInBackground() throws Exception {
+ int offset = 0;
+ int limit = 1000;
+ int processed = 0;
+ int total = -1;
+ boolean wroteHeader = false;
+
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
+ while (!isCancelled()) {
+ LookupAllValuesResponse page = LookupServiceClient.getInstance().getAllValues(selectedGroup.getId(), offset, limit, "");
+
+ List pageValues = page.getValues();
+ if (pageValues == null || pageValues.isEmpty()) {
+ break;
+ }
+
+ if (!wroteHeader) {
+ writer.write("key,value");
+ writer.newLine();
+ wroteHeader = true;
+ }
+
+ for (LookupValue value : pageValues) {
+ if (isCancelled()) {
+ break;
+ }
+
+ writer.write(String.format("%s,%s", escapeCsv(value.getKeyValue()), escapeCsv(value.getValueData())));
+ writer.newLine();
+
+ processed++;
+ if (total < 0) {
+ total = page.getTotalCount();
+ }
+
+ publishProgress(processed, total);
+ }
+
+ if (pageValues.size() < limit || isCancelled()) {
+ break;
+ }
+
+ offset += limit;
+ }
+
+ Frame.userPreferences.put("currentDirectory", file.getParent());
+
+ } catch (Exception e) {
+ logger.error("Failed to export values to CSV", e);
+ throw e;
+ }
+
+ return null;
+ }
+
+ private void publishProgress(int processed, int total) {
+ int progress = total > 0 ? (int) ((processed / (double) total) * 100) : 0;
+ progress = Math.min(progress, 100);
+
+ publish(new int[]{progress, processed, total});
+ }
+
+ @Override
+ protected void process(List chunks) {
+ if (!chunks.isEmpty()) {
+ int[] latest = chunks.get(chunks.size() - 1);
+ progressBar.setValue(latest[0]);
+ statusLabel.setText("Exported " + latest[1] + " of " + latest[2] + " entries");
+ }
+ }
+
+ @Override
+ protected void done() {
+ progressDialog.dispose();
+ if (isCancelled()) {
+ JOptionPane.showMessageDialog(parent, "Export cancelled by user.", "Cancelled", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+
+ try {
+ get(); // triggers exception handling if doInBackground failed
+
+ JOptionPane.showMessageDialog(parent, "CSV export completed.", "Export Complete", JOptionPane.INFORMATION_MESSAGE);
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ showError("Export failed: " + (cause.getMessage() != null ? cause.getMessage() : cause.getClass().getSimpleName()));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // restore interrupt status
+ showError("Export was interrupted.");
+ }
+ }
+ };
+
+ // Cancel button action
+ cancelButton.addActionListener(e -> exportWorker.cancel(true));
+
+ // Handle window close
+ progressDialog.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ int confirm = JOptionPane.showConfirmDialog(
+ progressDialog,
+ "Export is still in progress. Do you want to cancel?",
+ "Confirm Cancel",
+ JOptionPane.YES_NO_OPTION
+ );
+ if (confirm == JOptionPane.YES_OPTION) {
+ exportWorker.cancel(true);
+ }
+ }
+ });
+
+ exportWorker.execute();
+ progressDialog.setVisible(true);
+ }
+
+ private String escapeCsv(String input) {
+ if (input == null) return "";
+ if (input.contains(",") || input.contains("\"") || input.contains("\n")) {
+ input = input.replace("\"", "\"\"");
+ return "\"" + input + "\"";
+ }
+ return input;
+ }
+
+ private void showInformation(String msg) {
+ PlatformUI.MIRTH_FRAME.alertInformation(parent, msg);
+ }
+
+ private void showError(String err) {
+ PlatformUI.MIRTH_FRAME.alertError(parent, err);
+ }
+}
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/plugin/LookupTableClientPlugin.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/plugin/LookupTableClientPlugin.java
new file mode 100644
index 000000000..0e7ade9d4
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/plugin/LookupTableClientPlugin.java
@@ -0,0 +1,48 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.plugin;
+
+import com.mirth.connect.plugins.ClientPlugin;
+import com.mirth.connect.plugins.dynamiclookup.client.panel.DataStorePanel;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class LookupTableClientPlugin extends ClientPlugin {
+ private DataStorePanel dataStorePane;
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ public LookupTableClientPlugin(String name) {
+ super(name);
+
+ dataStorePane = new DataStorePanel();
+ }
+
+ @Override
+ public String getPluginPointName() {
+ return "Lookup Table Management System";
+ }
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void stop() {
+
+ }
+
+ @Override
+ public void reset() {
+
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/plugin/LookupTableReferencePlugin.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/plugin/LookupTableReferencePlugin.java
new file mode 100644
index 000000000..4ad8698cc
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/plugin/LookupTableReferencePlugin.java
@@ -0,0 +1,149 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.plugin;
+
+import com.mirth.connect.plugins.CodeTemplatePlugin;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.mirth.connect.model.codetemplates.CodeTemplate;
+import com.mirth.connect.model.codetemplates.CodeTemplateContextSet;
+import com.mirth.connect.model.codetemplates.CodeTemplateProperties.CodeTemplateType;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class LookupTableReferencePlugin extends CodeTemplatePlugin {
+ private static final Logger logger = LogManager.getLogger(LookupTableReferencePlugin.class);
+
+ public LookupTableReferencePlugin(String name) {
+ super(name);
+ }
+
+ @Override
+ public Map> getReferenceItems() {
+ Map> referenceItems = new HashMap>();
+
+ List templates = new ArrayList();
+
+ templates.add(new CodeTemplate(
+ "Lookup Value by Key",
+ CodeTemplateType.DRAG_AND_DROP_CODE,
+ CodeTemplateContextSet.getConnectorContextSet(),
+ "var value = LookupHelper.get(group, key);",
+ "Retrieves a value from the specified lookup group using the given key. Returns null if no match is found."
+ ));
+
+ templates.add(new CodeTemplate(
+ "Lookup Value by Key with TTL",
+ CodeTemplateType.DRAG_AND_DROP_CODE,
+ CodeTemplateContextSet.getConnectorContextSet(),
+ "var value = LookupHelper.get(group, key, ttlHours);",
+ "Retrieves a value from the specified lookup group using the given key and a TTL (in hours). "
+ + "If the cached or database value is older than the TTL, null is returned."
+ ));
+
+ templates.add(new CodeTemplate(
+ "Lookup Value with Default Fallback",
+ CodeTemplateType.DRAG_AND_DROP_CODE,
+ CodeTemplateContextSet.getConnectorContextSet(),
+ "var value = LookupHelper.get(group, key, defaultValue);",
+ "Retrieves a value from a lookup group, or returns the default if the group or key is missing."
+ ));
+
+ templates.add(new CodeTemplate(
+ "Lookup Value with TTL and Default Fallback",
+ CodeTemplateType.DRAG_AND_DROP_CODE,
+ CodeTemplateContextSet.getConnectorContextSet(),
+ "var value = LookupHelper.get(group, key, ttlHours, defaultValue);",
+ "Retrieves a value from a lookup group using the given key and TTL (in hours). "
+ + "If the value is missing or stale based on TTL, the default value is returned instead."
+ ));
+
+ templates.add(new CodeTemplate(
+ "Lookup Values Matching Pattern",
+ CodeTemplateType.DRAG_AND_DROP_CODE,
+ CodeTemplateContextSet.getConnectorContextSet(),
+ "var values = LookupHelper.getMatching(group, keyPattern);",
+ "Retrieves key-value pairs from the specified lookup group where keys match a pattern. Returns an empty map if the group does not exist or no matches are found."
+ ));
+
+ templates.add(new CodeTemplate(
+ "Batch Lookup by Keys",
+ CodeTemplateType.DRAG_AND_DROP_CODE,
+ CodeTemplateContextSet.getConnectorContextSet(),
+ "var keys = [\"key1\", \"key2\", \"key3\"];\nvar values = LookupHelper.getBatch(group, keys);",
+ "Retrieves multiple key-value pairs from the specified lookup group in a single operation. Returns an empty map if the group is not found or none of the keys exist."
+ ));
+
+ templates.add(new CodeTemplate(
+ "Batch Lookup by Keys with TTL",
+ CodeTemplateType.DRAG_AND_DROP_CODE,
+ CodeTemplateContextSet.getConnectorContextSet(),
+ "var keys = [\"key1\", \"key2\", \"key3\"];\nvar values = LookupHelper.getBatch(group, keys, ttlHours);",
+ "Retrieves multiple key-value pairs from the specified lookup group using a TTL (in hours). "
+ + "Only values updated within the TTL window will be returned. "
+ + "Returns an empty map if the group is not found or all values are stale or missing."
+ ));
+
+ templates.add(new CodeTemplate(
+ "Lookup Key Existence in Group",
+ CodeTemplateType.DRAG_AND_DROP_CODE,
+ CodeTemplateContextSet.getConnectorContextSet(),
+ "var found = LookupHelper.exists(group, key);",
+ "Checks whether the specified key exists in the given lookup group. Returns true if found; false if the group or key does not exist, or if an error occurs."
+ ));
+
+ templates.add(new CodeTemplate(
+ "Get Lookup Cache Statistics",
+ CodeTemplateType.DRAG_AND_DROP_CODE,
+ CodeTemplateContextSet.getConnectorContextSet(),
+ "var stats = LookupHelper.getCacheStats(group);",
+ "Retrieves cache and usage statistics for the specified lookup group, including hit/miss counts, hit rate, evictions, total lookups, and last accessed time. Returns an empty map if the group is not found or an error occurs."
+ ));
+
+ templates.add(new CodeTemplate(
+ "Set Lookup Value by Key",
+ CodeTemplateType.DRAG_AND_DROP_CODE,
+ CodeTemplateContextSet.getConnectorContextSet(),
+ "var success = LookupHelper.set(group, key, value);",
+ "Sets a value in the specified lookup group using the given key. Returns true if successful, false otherwise."
+ ));
+
+ // This defines the category
+ referenceItems.put("Lookup Table Functions", templates);
+
+ return referenceItems;
+ }
+
+ @Override
+ public String getPluginPointName() {
+ return "Lookup Table Reference Plugin";
+ }
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void stop() {
+
+ }
+
+ @Override
+ public void reset() {
+
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/service/LookupServiceClient.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/service/LookupServiceClient.java
new file mode 100644
index 000000000..862a3c72f
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/service/LookupServiceClient.java
@@ -0,0 +1,438 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.service;
+
+import com.mirth.connect.client.core.Client;
+import com.mirth.connect.client.core.ClientException;
+import com.mirth.connect.client.core.EntityException;
+import com.mirth.connect.client.ui.PlatformUI;
+import com.mirth.connect.plugins.dynamiclookup.client.exception.LookupApiClientException;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.request.LookupValueRequest;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.*;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.interfaces.LookupTableServletInterface;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.HistoryFilterState;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+import com.mirth.connect.plugins.dynamiclookup.shared.util.JsonUtils;
+import com.mirth.connect.plugins.dynamiclookup.shared.util.LookupErrorCode;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class LookupServiceClient {
+ private static LookupServiceClient instance = null;
+ private LookupTableServletInterface servlet;
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ public static LookupServiceClient getInstance() {
+ synchronized (LookupServiceClient.class) {
+ if (instance == null) {
+ instance = new LookupServiceClient();
+ }
+
+ return instance;
+ }
+ }
+
+ public LookupServiceClient() {
+ }
+
+ public List getAllGroups() throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().getAllGroups();
+
+ return JsonUtils.fromJsonList(response, LookupGroup.class);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to get all groups", e);
+ }
+ }
+
+ public LookupGroup getGroupById(Integer groupId) throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().getGroupById(groupId);
+
+ return JsonUtils.fromJson(response, LookupGroup.class);
+ } catch (ClientException e) {
+ // 3. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 4. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to get group by id", e);
+ }
+ }
+
+ public LookupGroup getGroupByName(String name) throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().getGroupByName(name);
+
+ return JsonUtils.fromJson(response, LookupGroup.class);
+ } catch (ClientException e) {
+ // 3. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 4. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to get group by name", e);
+ }
+ }
+
+ public LookupGroup createGroup(LookupGroup group) throws ClientException {
+ try {
+ // 1. Serialize the request body
+ String request = JsonUtils.toJson(group);
+
+ // 2. Make the call
+ String response = getServlet().createGroup(request);
+
+ return JsonUtils.fromJson(response, LookupGroup.class);
+ } catch (ClientException e) {
+ // 3. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 4. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to create group", e);
+ }
+ }
+
+ public LookupGroup updateGroup(LookupGroup group) throws ClientException {
+ try {
+ // 1. Serialize the request body
+ String request = JsonUtils.toJson(group);
+
+ // 2. Make the call
+ String response = getServlet().updateGroup(group.getId(), request);
+
+ return JsonUtils.fromJson(response, LookupGroup.class);
+ } catch (ClientException e) {
+ // 3. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 4. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to update group", e);
+ }
+ }
+
+ public void deleteGroup(Integer groupId) throws ClientException {
+ try {
+ // 1. Make the call
+ getServlet().deleteGroup(groupId);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to delete group", e);
+ }
+ }
+
+ public ImportLookupGroupResponse importGroup(boolean updateIfExists, String json) throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().importGroup(updateIfExists, json);
+
+ // 2. Parse successful response
+ return JsonUtils.fromJson(response, ImportLookupGroupResponse.class);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to import group", e);
+ }
+ }
+
+ public ExportLookupGroupResponse exportGroup(Integer groupId) throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().exportGroup(groupId);
+
+ // 2. Parse successful response
+ return JsonUtils.fromJson(response, ExportLookupGroupResponse.class);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to export group", e);
+ }
+ }
+
+ public ExportGroupPagedResponse exportGroupPaged(Integer groupId, int offset, int limit) throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().exportGroupPaged(groupId, offset, limit);
+
+ // 2. Parse successful response
+ return JsonUtils.fromJson(response, ExportGroupPagedResponse.class);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to export group (paged)", e);
+ }
+ }
+
+ public boolean checkValueExists(Integer groupId, String key) throws ClientException {
+ try {
+ String response = getServlet().getValue(groupId, key);
+ JsonUtils.fromJson(response, LookupValue.class);
+ return true;
+
+ } catch (ClientException e) {
+ try {
+ rethrowParsedClientError(e, false); // Silent mode: no logging
+ } catch (LookupApiClientException ex) {
+ if (LookupErrorCode.VALUE_NOT_FOUND.equalsIgnoreCase(ex.getError().getCode())) {
+ return false;
+ }
+ throw ex;
+ }
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected error while checking value existence", e);
+ }
+ }
+
+ public LookupValue getValue(Integer groupId, String key) throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().getValue(groupId, key);
+
+ // 2. Parse successful response
+ return JsonUtils.fromJson(response, LookupValue.class);
+
+ } catch (ClientException e) {
+ // 3. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 4. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to get value", e);
+ }
+ }
+
+ public LookupValueResponse setValue(Integer groupId, LookupValue value) throws ClientException {
+ try {
+ // 1. Serialize the request body
+ LookupValueRequest request = new LookupValueRequest();
+ request.setValue(value.getValueData());
+
+ // 2. Make the call
+ String response = getServlet().setValue(groupId, value.getKeyValue(), JsonUtils.toJson(request));
+
+ // 3. Parse successful response
+ return JsonUtils.fromJson(response, LookupValueResponse.class);
+
+ } catch (ClientException e) {
+ // 4. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 5. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to set value", e);
+ }
+ }
+
+ public void deleteValue(Integer groupId, String key) throws ClientException {
+ try {
+ // 1. Make the call
+ getServlet().deleteValue(groupId, key);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to delete value", e);
+ }
+ }
+
+ public LookupAllValuesResponse getAllValues(Integer groupId, int offset, int limit, String pattern) throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().getAllValues(groupId, offset, limit, pattern);
+
+ return JsonUtils.fromJson(response, LookupAllValuesResponse.class);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to get all values", e);
+ }
+ }
+
+ public ImportValuesResponse importValues(Integer groupId, boolean clearExisting, Map values) throws ClientException {
+ try {
+ // 1. Serialize the request body
+ Map request = new HashMap<>();
+ request.put("values", values);
+
+ // e. Make the call
+ String response = getServlet().importValues(groupId, clearExisting, JsonUtils.toJson(request));
+
+ return JsonUtils.fromJson(response, ImportValuesResponse.class);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to import values", e);
+ }
+ }
+
+ public GroupAuditEntriesResponse getAllAuditEntries(Integer groupId, int offset, int limit) throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().getGroupAuditEntries(groupId, offset, limit);
+
+ return JsonUtils.fromJson(response, GroupAuditEntriesResponse.class);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to get all audit entries", e);
+ }
+ }
+
+ public GroupAuditEntriesResponse searchAuditEntries(Integer groupId, int offset, int limit, HistoryFilterState filter) throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().searchGroupAuditEntries(groupId, offset, limit, filter.toJson());
+
+ return JsonUtils.fromJson(response, GroupAuditEntriesResponse.class);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to get search audit entries", e);
+ }
+ }
+
+ public GroupStatisticsResponse getGroupStatistics(Integer groupId) throws ClientException {
+ try {
+ // 1. Make the call
+ String response = getServlet().getGroupStatistics(groupId);
+
+ return JsonUtils.fromJson(response, GroupStatisticsResponse.class);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+
+ return null; // unreachable — rethrowParsedClientError always throws
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to get group statistics", e);
+ }
+ }
+
+ public void resetGroupStatistics(Integer groupId) throws ClientException {
+ try {
+ // 1. Make the call
+ getServlet().resetGroupStatistics(groupId);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to clear group statistics", e);
+ }
+ }
+
+ public void clearGroupCache(Integer groupId) throws ClientException {
+ try {
+ // 1. Make the call
+ getServlet().clearGroupCache(groupId);
+ } catch (ClientException e) {
+ // 2. Rethrow ClientException with parsed ErrorResponse if available
+ rethrowParsedClientError(e);
+ } catch (Exception e) {
+ // 3. JSON serialization or unexpected errors
+ throw new RuntimeException("Failed to clear group cache", e);
+ }
+ }
+
+ private LookupTableServletInterface getServlet() {
+ if (servlet == null) {
+ Client client = PlatformUI.MIRTH_FRAME.mirthClient;
+ servlet = client.getServlet(LookupTableServletInterface.class);
+ }
+
+ return servlet;
+ }
+
+ private void rethrowParsedClientError(ClientException e) throws ClientException {
+ rethrowParsedClientError(e, true); // default to logging enabled
+ }
+
+ private void rethrowParsedClientError(ClientException e, boolean logError) throws ClientException {
+ Throwable cause = e.getCause();
+
+ if (cause instanceof EntityException) {
+ String rawEntity = (String) ((EntityException) cause).getEntity();
+
+ ErrorResponse error;
+ try {
+ error = JsonUtils.fromJson(rawEntity, ErrorResponse.class);
+ if (logError) {
+ logger.error("Parsed API error: {}", JsonUtils.toJson(error));
+ }
+ } catch (Exception parseError) {
+ if (logError) {
+ logger.error("Failed to parse server error response: {}", rawEntity, parseError);
+ }
+ error = new ErrorResponse("UNPARSEABLE_RESPONSE", "Failed to parse server error");
+ }
+
+ throw new LookupApiClientException(error, e);
+ }
+
+ throw e; // fallback if not structured
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/util/CsvLineParser.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/util/CsvLineParser.java
new file mode 100644
index 000000000..d0eb9bc44
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/util/CsvLineParser.java
@@ -0,0 +1,56 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.util;
+
+import com.opencsv.CSVReader;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.StringReader;
+import java.util.AbstractMap;
+import java.util.Map;
+
+public class CsvLineParser {
+ private static final Logger logger = LogManager.getLogger(CsvLineParser.class);
+
+ /**
+ * Parses a CSV line assuming the first column is the key, and the second is always a string value.
+ * Handles quoted CSV fields and embedded commas properly.
+ *
+ * @param line A CSV line
+ * @param lineNumber The line number (optional for caller context)
+ * @return A Map.Entry of key to raw value string, or null if line is malformed
+ */
+ public static Map.Entry parseLine(String line, int lineNumber) {
+ try (CSVReader csvReader = new CSVReader(new StringReader(line))) {
+ String[] parts = csvReader.readNext();
+
+ if (parts == null || parts.length < 2) {
+ logger.debug("Skipping malformed CSV line at {}: '{}'", lineNumber, line);
+ return null; // malformed
+ }
+
+ String key = parts[0].trim();
+ String rawValue = parts[1].trim();
+
+ if (key.isEmpty() || rawValue.isEmpty()) {
+ logger.debug("Skipping line {} due to empty key or value: '{}'", lineNumber, line);
+ return null; // empty key or value
+ }
+
+ return new AbstractMap.SimpleEntry<>(key, rawValue);
+ } catch (Exception ex) {
+ logger.debug("Failed to parse line {}: '{}'. Error: {}", lineNumber, line, ex.getMessage());
+ return null; // error parsing
+ }
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/util/FileChooser.java b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/util/FileChooser.java
new file mode 100644
index 000000000..60a9ea609
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/client/src/main/java/com/mirth/connect/plugins/dynamiclookup/client/util/FileChooser.java
@@ -0,0 +1,73 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.client.util;
+
+import com.mirth.connect.client.ui.Frame;
+import org.apache.commons.io.FilenameUtils;
+
+import javax.swing.JFileChooser;
+import javax.swing.filechooser.FileFilter;
+
+import java.io.File;
+
+public class FileChooser {
+ public File createFileForExport(Frame parent, String defaultFileName, String fileExtension) {
+ JFileChooser exportFileChooser = new JFileChooser();
+ if (defaultFileName != null) {
+ exportFileChooser.setSelectedFile(new File(defaultFileName));
+ }
+
+ if (fileExtension != null) {
+ exportFileChooser.setFileFilter(new CustomFileFilter(fileExtension));
+ }
+
+ File currentDir = new File(Frame.userPreferences.get("currentDirectory", ""));
+ if (currentDir.exists()) {
+ exportFileChooser.setCurrentDirectory(currentDir);
+ }
+
+ if (exportFileChooser.showSaveDialog(parent) != 0) {
+ return null;
+ } else {
+ Frame.userPreferences.put("currentDirectory", exportFileChooser.getCurrentDirectory().getPath());
+ File exportFile = exportFileChooser.getSelectedFile();
+ if (exportFile.getName().length() < 4 || !FilenameUtils.getExtension(exportFile.getName()).equalsIgnoreCase(fileExtension)) {
+ exportFile = new File(exportFile.getAbsolutePath() + "." + fileExtension.toLowerCase());
+ }
+
+ return exportFile.exists() && !parent.alertOption(parent, "This file already exists. Would you like to overwrite it?") ? null : exportFile;
+ }
+ }
+
+ private class CustomFileFilter extends FileFilter {
+ private String fileExtension;
+
+ public CustomFileFilter(String fileExtension) {
+ this.fileExtension = fileExtension;
+ }
+
+ public boolean accept(File file) {
+ return file.isDirectory() || FilenameUtils.getExtension(file.getName()).equalsIgnoreCase(this.fileExtension);
+ }
+
+ public String getDescription() {
+ if (this.fileExtension.equalsIgnoreCase("csv")) {
+ return "CSV files";
+ }
+
+ if (this.fileExtension.equalsIgnoreCase("json")) {
+ return "JSON files";
+ }
+
+ return "All Files";
+ }
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/dynamic-lookup.properties b/custom-extensions/dynamic-lookup-gateway/dynamic-lookup.properties
new file mode 100644
index 000000000..845163abf
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/dynamic-lookup.properties
@@ -0,0 +1,32 @@
+# If true, the plugin will use an external database defined below.
+# If false, it will reuse Mirth Connect's internal database connection.
+useExternalDb=false
+
+# options: derby, mysql, postgres, oracle, sqlserver
+database = postgres
+
+# examples:
+# Derby jdbc:derby:${dir.appdata}/mirthdb;create=true
+# PostgreSQL jdbc:postgresql://localhost:5432/mirthdb
+# MySQL jdbc:mysql://localhost:3306/mirthdb
+# Oracle jdbc:oracle:thin:@localhost:1521:DB
+# SQL Server/Sybase (jTDS) jdbc:jtds:sqlserver://localhost:1433/mirthdb
+# Microsoft SQL Server jdbc:sqlserver://localhost:1433;databaseName=mirthdb
+# If you are using the Microsoft SQL Server driver, please also specify database.driver below
+database.url = jdbc:postgresql://localhost:5432/mirthdb
+
+# If using a custom or non-default driver, specify it here.
+# example:
+# Microsoft SQL server: database.driver = com.microsoft.sqlserver.jdbc.SQLServerDriver
+# (Note: the jTDS driver is used by default for sqlserver)
+#database.driver =
+
+# Maximum number of connections allowed for the main read/write connection pool
+database.max-connections = 20
+
+# database credentials
+database.username =
+database.password =
+
+#On startup, Maximum number of retries to establish database connections in case of failure
+database.connection.maxretry = 2
diff --git a/custom-extensions/dynamic-lookup-gateway/libs/jfreechart-1.5.6.jar b/custom-extensions/dynamic-lookup-gateway/libs/jfreechart-1.5.6.jar
new file mode 100644
index 000000000..eabb86477
Binary files /dev/null and b/custom-extensions/dynamic-lookup-gateway/libs/jfreechart-1.5.6.jar differ
diff --git a/custom-extensions/dynamic-lookup-gateway/libs/opencsv-5.9.jar b/custom-extensions/dynamic-lookup-gateway/libs/opencsv-5.9.jar
new file mode 100644
index 000000000..de5fe0487
Binary files /dev/null and b/custom-extensions/dynamic-lookup-gateway/libs/opencsv-5.9.jar differ
diff --git a/custom-extensions/dynamic-lookup-gateway/plugin.xml b/custom-extensions/dynamic-lookup-gateway/plugin.xml
new file mode 100644
index 000000000..741557134
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/plugin.xml
@@ -0,0 +1,53 @@
+
+
+ Dynamic Lookup Gateway
+ Daniel Svanstedt, Thai Tran
+ 1.0.0
+ 4.5.3, 4.5.4
+ https://www.innovarhealthcare.com
+ This plugin provides a centralized repository for key-value pairs used across channel
+
+ com.mirth.connect.plugins.dynamiclookup.client.plugin.LookupTableClientPlugin
+
+
+ com.mirth.connect.plugins.dynamiclookup.server.plugin.LookupTableServicePlugin
+
+
+
+
+
+
+
+
+ com.mirth.connect.plugins.dynamiclookup.client.plugin.LookupTableReferencePlugin
+
+ com.mirth.connect.plugins.dynamiclookup.server.userutil
+
+
+
+ all
+ mapper/lookup.xml
+
+
+ derby
+ mapper/derby-lookup.xml
+
+
+ mysql
+ mapper/mysql-lookup.xml
+
+
+ oracle
+ mapper/oracle-lookup.xml
+
+
+ postgres
+ mapper/postgres-lookup.xml
+
+
+ sqlserver
+ mapper/sqlserver-lookup.xml
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/CachedValue.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/CachedValue.java
new file mode 100644
index 000000000..308fc7486
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/CachedValue.java
@@ -0,0 +1,44 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.cache;
+
+import java.util.Date;
+
+public class CachedValue {
+
+ private final String value;
+ private final Date updatedAt;
+
+ /**
+ * Creates a new CachedValue instance.
+ *
+ * @param value The actual value associated with the key.
+ * @param updatedAt The timestamp from the database indicating when the value was last updated.
+ */
+ public CachedValue(String value, Date updatedAt) {
+ this.value = value;
+ this.updatedAt = updatedAt;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public Date getUpdatedAt() {
+ return updatedAt;
+ }
+
+ @Override
+ public String toString() {
+ return "CachedValue{value='" + value + "', updatedAt=" + updatedAt + '}';
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/FifoCache.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/FifoCache.java
new file mode 100644
index 000000000..b7b87a42f
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/FifoCache.java
@@ -0,0 +1,29 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.cache;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class FifoCache extends LinkedHashMap {
+ private final int maxSize;
+
+ public FifoCache(int maxSize) {
+ super(maxSize, 0.75f, false); // false = insertion order
+ this.maxSize = maxSize;
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > maxSize;
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/FifoCacheWrapper.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/FifoCacheWrapper.java
new file mode 100644
index 000000000..e64b7234b
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/FifoCacheWrapper.java
@@ -0,0 +1,48 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.cache;
+
+import java.util.Collections;
+import java.util.Map;
+
+public class FifoCacheWrapper implements SimpleCache {
+ private final Map map;
+
+ public FifoCacheWrapper(int maxSize) {
+ this.map = Collections.synchronizedMap(new FifoCache<>(maxSize));
+ }
+
+ @Override
+ public V get(K key) {
+ return map.get(key);
+ }
+
+ @Override
+ public void put(K key, V value) {
+ map.put(key, value);
+ }
+
+ @Override
+ public void remove(K key) {
+ map.remove(key);
+ }
+
+ @Override
+ public void clear() {
+ map.clear();
+ }
+
+ @Override
+ public int size() {
+ return map.size();
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/GuavaCacheWrapper.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/GuavaCacheWrapper.java
new file mode 100644
index 000000000..7b7a616e9
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/GuavaCacheWrapper.java
@@ -0,0 +1,51 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.cache;
+
+import com.google.common.cache.Cache;
+
+public class GuavaCacheWrapper implements SimpleCache {
+ private final Cache cache;
+
+ public GuavaCacheWrapper(Cache cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public V get(K key) {
+ return cache.getIfPresent(key);
+ }
+
+ @Override
+ public void put(K key, V value) {
+ cache.put(key, value);
+ }
+
+ @Override
+ public void remove(K key) {
+ cache.invalidate(key);
+ }
+
+ @Override
+ public void clear() {
+ cache.invalidateAll();
+ }
+
+ @Override
+ public int size() {
+ return cache.asMap().size();
+ }
+
+ public Cache getGuavaCache() {
+ return this.cache;
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/LookupCacheManager.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/LookupCacheManager.java
new file mode 100644
index 000000000..cf8cc9845
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/LookupCacheManager.java
@@ -0,0 +1,153 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.cache;
+
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupGroupDao;
+import com.mirth.connect.plugins.dynamiclookup.server.exception.GroupNotFoundException;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+
+import com.google.common.cache.CacheStats;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.mirth.connect.plugins.dynamiclookup.shared.util.TtlUtils;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Date;
+
+public class LookupCacheManager {
+ private final Map> groupCaches = new ConcurrentHashMap<>();
+ private final LookupGroupDao groupDao;
+
+ public LookupCacheManager(LookupGroupDao groupDao) {
+ this.groupDao = groupDao;
+ }
+
+ /**
+ * Gets a value from cache if present.
+ * If ttlHours > 0, the value must be within the TTL based on updatedAt.
+ * If ttlHours == 0, TTL is ignored and any cached value is returned.
+ */
+ public String getValue(int groupId, String key, long ttlHours) {
+ SimpleCache cache = getOrCreateCache(groupId);
+ CachedValue cached = cache.get(key);
+
+ if (cached == null) {
+ return null;
+ }
+
+ // Use shared TTL logic
+ if (TtlUtils.isWithinTtl(cached.getUpdatedAt(), ttlHours)) {
+ return cached.getValue();
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Adds or updates a value in cache using updatedAt from the database.
+ */
+ public void putValue(int groupId, String key, String value, Date updatedAt) {
+ SimpleCache cache = getOrCreateCache(groupId);
+ cache.put(key, new CachedValue(value, updatedAt));
+ }
+
+ /**
+ * Removes a specific value from cache
+ */
+ public void removeValue(int groupId, String key) {
+ SimpleCache cache = getOrCreateCache(groupId);
+ cache.remove(key);
+ }
+
+ /**
+ * Clears all values for a specific group
+ */
+ public void clearGroupCache(int groupId) {
+ SimpleCache cache = groupCaches.get(groupId);
+ if (cache != null) {
+ cache.clear();
+ }
+ }
+
+ /**
+ * Clears all caches
+ */
+ public void clearAllCaches() {
+ groupCaches.values().forEach(SimpleCache::clear);
+ }
+
+ /**
+ * Gets the current number of entries in the group's cache.
+ */
+ public int getCacheSize(int groupId) {
+ SimpleCache cache = groupCaches.get(groupId);
+ return (cache != null) ? cache.size() : 0;
+ }
+
+ /**
+ * Gets the configured maximum size of the group's cache.
+ */
+ public int getCacheMaxSize(int groupId) {
+ LookupGroup group = groupDao.getGroupById(groupId);
+ return (group != null) ? group.getCacheSize() : -1;
+ }
+
+ /**
+ * Gets cache statistics for a group (only available for LRU caches).
+ */
+ public CacheStats getCacheStats(int groupId) {
+ SimpleCache cache = groupCaches.get(groupId);
+
+ if (cache instanceof GuavaCacheWrapper) {
+ GuavaCacheWrapper guava = (GuavaCacheWrapper) cache;
+ return guava.getGuavaCache().stats();
+ }
+
+ // FIFO or unknown cache types do not support stats
+ return null;
+ }
+
+ /**
+ * Creates or retrieves the cache for a group
+ */
+ private SimpleCache getOrCreateCache(int groupId) {
+ return groupCaches.computeIfAbsent(groupId, k -> {
+ LookupGroup group = groupDao.getGroupById(groupId);
+ if (group == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+ return createCacheForGroup(group);
+ });
+ }
+
+ /**
+ * Creates a cache with the appropriate eviction policy
+ */
+ private SimpleCache createCacheForGroup(LookupGroup group) {
+ int cacheSize = group.getCacheSize();
+ String cachePolicy = group.getCachePolicy();
+
+ if ("FIFO".equalsIgnoreCase(cachePolicy)) {
+ return new FifoCacheWrapper<>(cacheSize);
+ } else { // Default to LRU
+ Cache guavaCache = CacheBuilder.newBuilder()
+ .maximumSize(cacheSize)
+// .expireAfterWrite(10, TimeUnit.MINUTES) // TTL (optional)
+ .recordStats()
+ .build();
+ return new GuavaCacheWrapper<>(guavaCache);
+ }
+ }
+}
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/SimpleCache.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/SimpleCache.java
new file mode 100644
index 000000000..1d921b55b
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/cache/SimpleCache.java
@@ -0,0 +1,24 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.cache;
+
+public interface SimpleCache {
+ V get(K key);
+
+ void put(K key, V value);
+
+ void remove(K key);
+
+ void clear();
+
+ int size();
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/config/DatabaseSettings.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/config/DatabaseSettings.java
new file mode 100644
index 000000000..bf6e59587
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/config/DatabaseSettings.java
@@ -0,0 +1,150 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.config;
+
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Properties;
+import java.util.Map;
+import java.util.HashMap;
+
+public class DatabaseSettings {
+ private static Map DEFAULT_DRIVER_MAP = new HashMap<>();
+
+ static {
+ DEFAULT_DRIVER_MAP.put("derby", "org.apache.derby.jdbc.EmbeddedDriver");
+ DEFAULT_DRIVER_MAP.put("mysql", "com.mysql.cj.jdbc.Driver");
+ DEFAULT_DRIVER_MAP.put("oracle", "oracle.jdbc.OracleDriver");
+ DEFAULT_DRIVER_MAP.put("postgres", "org.postgresql.Driver");
+ DEFAULT_DRIVER_MAP.put("sqlserver", "net.sourceforge.jtds.jdbc.Driver");
+ }
+
+ private boolean useExternalDb;
+ private String database;
+ private String url;
+ private String username;
+ private String password;
+ private String driver;
+ private int maxConnections;
+ private int maxRetry;
+
+ // Getters and setters
+
+ public boolean isUseExternalDb() {
+ return useExternalDb;
+ }
+
+ public void setUseExternalDb(boolean useExternalDb) {
+ this.useExternalDb = useExternalDb;
+ }
+
+ public String getDatabase() {
+ return database;
+ }
+
+ public void setDatabase(String database) {
+ this.database = database;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getDriver() {
+ return driver;
+ }
+
+ public void setDriver(String driver) {
+ this.driver = driver;
+ }
+
+ public int getMaxConnections() {
+ return maxConnections;
+ }
+
+ public void setMaxConnections(int maxConnections) {
+ this.maxConnections = maxConnections;
+ }
+
+ public int getMaxRetry() {
+ return maxRetry;
+ }
+
+ public void setMaxRetry(int maxRetry) {
+ this.maxRetry = maxRetry;
+ }
+
+ private String getMappedDatabaseDriver() {
+ if (StringUtils.isBlank(driver)) {
+ return MapUtils.getString(DEFAULT_DRIVER_MAP, getDatabase());
+ } else {
+ return driver;
+ }
+ }
+
+ public Properties getProperties() {
+ Properties properties = new Properties();
+
+ if (getMappedDatabaseDriver() != null) {
+ properties.setProperty("driver", getMappedDatabaseDriver());
+ }
+ if (url != null) {
+ properties.setProperty("url", url);
+ }
+ if (username != null) {
+ properties.setProperty("username", username);
+ }
+ if (password != null) {
+ properties.setProperty("password", password);
+ }
+
+ properties.setProperty("database.max-connections", String.valueOf(maxConnections));
+ properties.setProperty("database.connection.maxretry", String.valueOf(maxRetry));
+
+ return properties;
+ }
+
+ @Override
+ public String toString() {
+ return "DatabaseSettings{" +
+ "useExternalDb=" + useExternalDb +
+ ", database='" + database + '\'' +
+ ", url='" + url + '\'' +
+ ", username='" + username + '\'' +
+ ", password='" + password + '\'' +
+ ", driver='" + driver + '\'' +
+ ", maxConnections=" + maxConnections +
+ ", maxRetry=" + maxRetry +
+ '}';
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/config/DatabaseSettingsLoader.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/config/DatabaseSettingsLoader.java
new file mode 100644
index 000000000..f58fd5b33
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/config/DatabaseSettingsLoader.java
@@ -0,0 +1,51 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.config;
+
+import com.mirth.connect.client.core.PropertiesConfigurationUtil;
+import com.mirth.connect.server.tools.ClassPathResource;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.File;
+
+import org.apache.commons.configuration2.PropertiesConfiguration;
+
+public class DatabaseSettingsLoader {
+ private static final Logger logger = LogManager.getLogger(DatabaseSettingsLoader.class);
+ private static final String CONFIG_PATH = "dynamic-lookup.properties";
+
+ public static DatabaseSettings load() {
+ DatabaseSettings settings = new DatabaseSettings();
+
+ try {
+ PropertiesConfiguration config = PropertiesConfigurationUtil.create(new File(ClassPathResource.getResourceURI(CONFIG_PATH)));
+
+ settings.setUseExternalDb(config.getBoolean("useExternalDb", false));
+ settings.setDatabase(config.getString("database", ""));
+ settings.setUrl(config.getString("database.url", ""));
+ settings.setUsername(config.getString("database.username", ""));
+ settings.setPassword(config.getString("database.password", ""));
+ settings.setDriver(config.getString("database.driver", ""));
+ settings.setMaxConnections(config.getInt("database.max-connections", 20));
+ settings.setMaxRetry(config.getInt("database.connection.maxretry", 2));
+
+ logger.info("Loaded database settings from {}", CONFIG_PATH);
+ } catch (Exception e) {
+ logger.warn("Failed to load database settings from {}", CONFIG_PATH, e);
+
+ settings.setUseExternalDb(false);
+ }
+
+ return settings;
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupAuditDao.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupAuditDao.java
new file mode 100644
index 000000000..b8e928de6
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupAuditDao.java
@@ -0,0 +1,30 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.dao;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.HistoryFilterState;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupAudit;
+
+import java.util.List;
+
+public interface LookupAuditDao {
+ void insertAuditEntry(LookupAudit audit);
+
+ List getAuditEntriesByGroup(int groupId, int offset, int limit);
+
+ List searchAuditEntriesByGroup(int groupId, int offset, int limit, HistoryFilterState filter);
+
+ List getAuditEntriesByKey(int groupId, String keyValue, int limit);
+
+ long getAuditEntryCount(int groupId);
+
+ long searchAuditEntryCount(int groupId, HistoryFilterState filter);
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupGroupDao.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupGroupDao.java
new file mode 100644
index 000000000..26e4c90fc
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupGroupDao.java
@@ -0,0 +1,38 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.dao;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+
+import java.util.List;
+
+public interface LookupGroupDao {
+ // Group CRUD operations
+ LookupGroup getGroupById(int id);
+
+ LookupGroup getGroupByName(String name);
+
+ List getAllGroups();
+
+ int insertGroup(LookupGroup group);
+
+ void updateGroup(LookupGroup group);
+
+ void deleteGroup(int id);
+
+ // Dynamic table management
+ void createValueTable(String tableName);
+
+ void dropValueTable(String tableName);
+
+ boolean tableExists(String tableName);
+
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupStatisticsDao.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupStatisticsDao.java
new file mode 100644
index 000000000..60fa2fd26
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupStatisticsDao.java
@@ -0,0 +1,27 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.dao;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupStatistics;
+
+import java.util.List;
+
+public interface LookupStatisticsDao {
+ void insertStatistics(int groupId);
+
+ void updateStatistics(int groupId, boolean cacheHit);
+
+ LookupStatistics getStatistics(int groupId);
+
+ void resetStatistics(int groupId);
+
+ List getAllStatistics();
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupValueDao.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupValueDao.java
new file mode 100644
index 000000000..e0767dfe8
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/LookupValueDao.java
@@ -0,0 +1,45 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.dao;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+
+import java.util.List;
+import java.util.Map;
+
+public interface LookupValueDao {
+ // Value operations
+ LookupValue getLookupValue(String tableName, String keyValue);
+
+ String getValue(String tableName, String keyValue);
+
+ List getAllValues(String tableName);
+
+ List searchLookupValues(String tableName, Integer offset, Integer limit, String pattern);
+
+ List getMatchingValues(String tableName, String keyPattern);
+
+ List getKeys(String tableName, String keyPattern);
+
+ void insertValue(String tableName, String keyValue, String valueData);
+
+ void updateValue(String tableName, String keyValue, String valueData);
+
+ void deleteValue(String tableName, String keyValue);
+
+ void deleteAllValues(String tableName);
+
+ int importValues(String tableName, Map values);
+
+ long getValueCount(String tableName);
+
+ long searchLookupValuesCount(String tableName, String pattern);
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupAuditDao.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupAuditDao.java
new file mode 100644
index 000000000..dd4f5eaf3
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupAuditDao.java
@@ -0,0 +1,130 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.dao.impl;
+
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupAuditDao;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.HistoryFilterState;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupAudit;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionManager;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MyBatisLookupAuditDao implements LookupAuditDao {
+ private SqlSessionManager sqlSessionManager;
+
+ public MyBatisLookupAuditDao(SqlSessionManager sqlSessionManager) {
+ this.sqlSessionManager = sqlSessionManager;
+ }
+
+ @Override
+ public void insertAuditEntry(LookupAudit audit) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("groupId", audit.getGroupId());
+ params.put("tableName", audit.getTableName());
+ params.put("keyValue", audit.getKeyValue());
+ params.put("action", audit.getAction());
+ params.put("oldValue", audit.getOldValue());
+ params.put("newValue", audit.getNewValue());
+ params.put("userId", audit.getUserId());
+ session.insert("Lookup.insertAuditEntry", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public List getAuditEntriesByGroup(int groupId, int offset, int limit) {
+ SqlSession session = sqlSessionManager.openSession();
+
+ try {
+ Map params = new HashMap<>();
+ params.put("groupId", groupId);
+ params.put("offset", offset);
+ params.put("limit", limit);
+ return session.selectList("Lookup.getAuditEntriesByGroup", params);
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public List searchAuditEntriesByGroup(int groupId, int offset, int limit, HistoryFilterState filter) {
+ SqlSession session = sqlSessionManager.openSession();
+
+ try {
+ Map params = new HashMap<>();
+ params.put("groupId", groupId);
+ params.put("offset", offset);
+ params.put("limit", limit);
+
+ // Add filters from HistoryFilterState
+ params.put("keyValue", filter.getKeyValue());
+ params.put("action", filter.getAction());
+ params.put("userId", filter.getUserId());
+ params.put("startDate", filter.getStartDate());
+ params.put("endDate", filter.getEndDate());
+ return session.selectList("Lookup.searchAuditEntriesByGroup", params);
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public List getAuditEntriesByKey(int groupId, String keyValue, int limit) {
+ return new java.util.ArrayList<>();
+ }
+
+ @Override
+ public long getAuditEntryCount(int groupId) {
+ SqlSession session = sqlSessionManager.openSession();
+ try {
+ Map params = new HashMap<>();
+ params.put("groupId", groupId);
+ return session.selectOne("Lookup.getAuditEntryCount", params);
+ } finally {
+ session.close(); // Ensure session is always closed
+ }
+ }
+
+ @Override
+ public long searchAuditEntryCount(int groupId, HistoryFilterState filter) {
+ SqlSession session = sqlSessionManager.openSession();
+ try {
+ Map params = new HashMap<>();
+ params.put("groupId", groupId);
+
+ // Add filters from HistoryFilterState
+ params.put("keyValue", filter.getKeyValue());
+ params.put("action", filter.getAction());
+ params.put("userId", filter.getUserId());
+ params.put("startDate", filter.getStartDate());
+ params.put("endDate", filter.getEndDate());
+ return session.selectOne("Lookup.searchAuditEntryCount", params);
+ } finally {
+ session.close(); // Ensure session is always closed
+ }
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupGroupDao.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupGroupDao.java
new file mode 100644
index 000000000..1c1ab0932
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupGroupDao.java
@@ -0,0 +1,196 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.dao.impl;
+
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupGroupDao;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionManager;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MyBatisLookupGroupDao implements LookupGroupDao {
+ private SqlSessionManager sqlSessionManager;
+
+ public MyBatisLookupGroupDao(SqlSessionManager sqlSessionManager) {
+ this.sqlSessionManager = sqlSessionManager;
+ }
+
+ @Override
+ public LookupGroup getGroupById(int id) {
+ SqlSession session = sqlSessionManager.openSession();
+ try {
+ return session.selectOne("Lookup.getGroupById", id);
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public LookupGroup getGroupByName(String name) {
+ SqlSession session = sqlSessionManager.openSession();
+ try {
+ return session.selectOne("Lookup.getGroupByName", name);
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public List getAllGroups() {
+ SqlSession session = sqlSessionManager.openSession();
+ try {
+ return session.selectList("Lookup.getAllGroups");
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public int insertGroup(LookupGroup group) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("name", group.getName());
+ params.put("description", group.getDescription());
+ params.put("version", group.getVersion());
+ params.put("cacheSize", group.getCacheSize());
+ params.put("cachePolicy", group.getCachePolicy());
+ session.insert("Lookup.insertGroup", params);
+ session.commit();
+ commitSuccess = true;
+
+ Object idObj = params.get("id");
+ if (idObj instanceof BigDecimal) {
+ return ((BigDecimal) idObj).intValue();
+ } else if (idObj instanceof BigInteger) {
+ return ((BigInteger) idObj).intValue();
+ } else if (idObj instanceof Integer) {
+ return (Integer) idObj;
+ } else {
+ throw new IllegalStateException("Unexpected ID type: " + idObj.getClass());
+ }
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public void updateGroup(LookupGroup group) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("id", group.getId());
+ params.put("name", group.getName());
+ params.put("description", group.getDescription());
+ params.put("version", group.getVersion());
+ params.put("cacheSize", group.getCacheSize());
+ params.put("cachePolicy", group.getCachePolicy());
+
+ session.update("Lookup.updateGroup", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public void deleteGroup(int id) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ session.delete("Lookup.deleteGroup", id);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+
+ @Override
+ public void createValueTable(String tableName) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ session.update("Lookup.createLookupValueTable", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public void dropValueTable(String tableName) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ session.update("Lookup.dropLookupValueTable", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public boolean tableExists(String tableName) {
+ return false;
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupStatisticsDao.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupStatisticsDao.java
new file mode 100644
index 000000000..2308db000
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupStatisticsDao.java
@@ -0,0 +1,118 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.dao.impl;
+
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupStatisticsDao;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupStatistics;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionManager;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MyBatisLookupStatisticsDao implements LookupStatisticsDao {
+ private SqlSessionManager sqlSessionManager;
+
+ public MyBatisLookupStatisticsDao(SqlSessionManager sqlSessionManager) {
+ this.sqlSessionManager = sqlSessionManager;
+ }
+
+ @Override
+ public void insertStatistics(int groupId) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("groupId", groupId);
+ session.insert("Lookup.insertStatistics", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public void updateStatistics(int groupId, boolean cacheHit) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("groupId", groupId);
+ params.put("cacheHit", cacheHit);
+ params.put("lastAccessed", new Date());
+
+ session.update("Lookup.updateStatistics", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public LookupStatistics getStatistics(int groupId) {
+ SqlSession session = sqlSessionManager.openSession();
+ try {
+ return session.selectOne("Lookup.getStatistics", groupId);
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public void resetStatistics(int groupId) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("groupId", groupId);
+ params.put("resetDate", new Date());
+
+ session.update("Lookup.resetStatistics", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public List getAllStatistics() {
+ return new java.util.ArrayList<>();
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupValueDao.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupValueDao.java
new file mode 100644
index 000000000..61ee65427
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/dao/impl/MyBatisLookupValueDao.java
@@ -0,0 +1,256 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.dao.impl;
+
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupValueDao;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionManager;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MyBatisLookupValueDao implements LookupValueDao {
+ private SqlSessionManager sqlSessionManager;
+
+ public MyBatisLookupValueDao(SqlSessionManager sqlSessionManager) {
+ this.sqlSessionManager = sqlSessionManager;
+ }
+
+ @Override
+ public LookupValue getLookupValue(String tableName, String keyValue) {
+ SqlSession session = sqlSessionManager.openSession();
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ params.put("keyValue", keyValue);
+ return session.selectOne("Lookup.getLookupValue", params);
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public String getValue(String tableName, String keyValue) {
+ SqlSession session = sqlSessionManager.openSession();
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ params.put("keyValue", keyValue);
+ return session.selectOne("Lookup.getValue", params);
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public List getAllValues(String tableName) {
+ SqlSession session = sqlSessionManager.openSession();
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ return session.selectList("Lookup.getLookupValues", params);
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public List searchLookupValues(String tableName, Integer offset, Integer limit, String pattern) {
+ SqlSession session = sqlSessionManager.openSession();
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ params.put("offset", offset);
+ params.put("limit", limit);
+ params.put("pattern", pattern);
+ return session.selectList("Lookup.searchLookupValues", params);
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public List getMatchingValues(String tableName, String keyPattern) {
+ SqlSession session = sqlSessionManager.openSession();
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ params.put("keyPattern", keyPattern);
+ return session.selectList("Lookup.getMatchingLookupValues", params);
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public List getKeys(String tableName, String keyPattern) {
+ return new java.util.ArrayList<>();
+ }
+
+ @Override
+ public void insertValue(String tableName, String keyValue, String valueData) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ params.put("keyValue", keyValue);
+ params.put("valueData", valueData);
+ session.insert("Lookup.insertValue", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public void updateValue(String tableName, String keyValue, String valueData) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ params.put("keyValue", keyValue);
+ params.put("valueData", valueData);
+ session.insert("Lookup.updateValue", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public void deleteValue(String tableName, String keyValue) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ params.put("keyValue", keyValue);
+ session.delete("Lookup.deleteValue", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public void deleteAllValues(String tableName) {
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ session.delete("Lookup.deleteAllValues", params);
+ session.commit();
+ commitSuccess = true;
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+ }
+
+ @Override
+ public int importValues(String tableName, Map values) {
+ int affectedRows = 0;
+ SqlSession session = sqlSessionManager.openSession();
+ boolean commitSuccess = false;
+
+ try {
+ for (Map.Entry entry : values.entrySet()) {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ params.put("keyValue", entry.getKey());
+ params.put("valueData", entry.getValue());
+ try {
+ session.insert("Lookup.insertValue", params);
+ affectedRows++;
+ } catch (Exception ignored) {
+ }
+ }
+
+ session.commit();
+ } finally {
+ if (!commitSuccess) {
+ try {
+ session.rollback();
+ } catch (Exception ignored) {
+ }
+ }
+ session.close();
+ }
+
+ return affectedRows;
+ }
+
+ @Override
+ public long getValueCount(String tableName) {
+ SqlSession session = sqlSessionManager.openSession();
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ return session.selectOne("Lookup.getValueCount", params);
+ } finally {
+ session.close(); // Ensure session is always closed
+ }
+ }
+
+ @Override
+ public long searchLookupValuesCount(String tableName, String pattern) {
+ SqlSession session = sqlSessionManager.openSession();
+ try {
+ Map params = new HashMap<>();
+ params.put("tableName", tableName);
+ params.put("pattern", pattern);
+ return session.selectOne("Lookup.searchLookupValuesCount", params);
+ } finally {
+ session.close(); // Ensure session is always closed
+ }
+ }
+
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/DuplicateGroupNameException.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/DuplicateGroupNameException.java
new file mode 100644
index 000000000..ecdfca367
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/DuplicateGroupNameException.java
@@ -0,0 +1,17 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.exception;
+
+public class DuplicateGroupNameException extends RuntimeException {
+ public DuplicateGroupNameException(String message) {
+ super(message);
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/GroupNotFoundException.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/GroupNotFoundException.java
new file mode 100644
index 000000000..0a139ad4c
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/GroupNotFoundException.java
@@ -0,0 +1,17 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.exception;
+
+public class GroupNotFoundException extends RuntimeException {
+ public GroupNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/LookupApiException.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/LookupApiException.java
new file mode 100644
index 000000000..c136e106e
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/LookupApiException.java
@@ -0,0 +1,73 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.exception;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.ErrorResponseFactory;
+import com.mirth.connect.plugins.dynamiclookup.shared.util.JsonUtils;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.time.ZonedDateTime;
+import java.time.ZoneOffset;
+
+public class LookupApiException extends WebApplicationException {
+
+ // Basic constructor: 500 without message
+ public LookupApiException() {
+ super(buildJsonResponse(Status.INTERNAL_SERVER_ERROR, "UNKNOWN_ERROR", "An unexpected error occurred."));
+ }
+
+ // Status only (use with caution — no code/message)
+ public LookupApiException(Status status) {
+ super(buildJsonResponse(status, status.name(), status.getReasonPhrase()));
+ }
+
+ // Full detail
+ public LookupApiException(Status status, String code, String message) {
+ super(buildJsonResponse(status, code, message));
+ }
+
+ // Custom HTTP status code
+ public LookupApiException(int statusCode, String code, String message) {
+ super(buildJsonResponse(Status.fromStatusCode(statusCode), code, message));
+ }
+
+ // Wrap another exception
+ public LookupApiException(Throwable cause) {
+ super(buildJsonResponse(Status.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", cause.getMessage()));
+ }
+
+ // Accept prebuilt response (for advanced use)
+ public LookupApiException(Response response) {
+ super(response);
+ }
+
+ private static Response buildJsonResponse(Status status, String code, String message) {
+ try {
+ return Response.status(status)
+ .type(MediaType.APPLICATION_JSON)
+ .entity(JsonUtils.toJson(ErrorResponseFactory.build(code, message)))
+ .build();
+
+ } catch (Exception e) {
+ // Fallback: plain JSON string in case serialization fails
+ String fallbackJson = "{\"status\":\"error\",\"code\":\"INTERNAL_ERROR\",\"message\":\"Failed to serialize error response\",\"timestamp\":\"" +
+ ZonedDateTime.now(ZoneOffset.UTC).toString() + "\"}";
+ return Response.status(status)
+ .type(MediaType.APPLICATION_JSON)
+ .entity(fallbackJson)
+ .build();
+ }
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/ValueOperationException.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/ValueOperationException.java
new file mode 100644
index 000000000..d1cb3bd10
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/ValueOperationException.java
@@ -0,0 +1,30 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.exception;
+
+public class ValueOperationException extends RuntimeException {
+ private final Throwable cause;
+
+ public ValueOperationException(String message, Throwable cause) {
+ super(message);
+ this.cause = cause;
+ }
+
+ public ValueOperationException(String message) {
+ this(message, null);
+ }
+
+ @Override
+ public Throwable getCause() {
+ return cause;
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/ValueTableCreationException.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/ValueTableCreationException.java
new file mode 100644
index 000000000..f62131c10
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/exception/ValueTableCreationException.java
@@ -0,0 +1,29 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.exception;
+
+public class ValueTableCreationException extends RuntimeException {
+ private final Throwable cause;
+
+ public ValueTableCreationException(String message, Throwable cause) {
+ super(message);
+ this.cause = cause;
+ }
+
+ public ValueTableCreationException(String message) {
+ this(message, null);
+ }
+
+ @Override
+ public Throwable getCause() {
+ return cause;
+ }
+}
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/plugin/LookupTableServicePlugin.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/plugin/LookupTableServicePlugin.java
new file mode 100644
index 000000000..6dda18181
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/plugin/LookupTableServicePlugin.java
@@ -0,0 +1,317 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.plugin;
+
+import static com.mirth.connect.plugins.dynamiclookup.shared.interfaces.LookupTableServletInterface.PERMISSION_ACCESS;
+
+import com.mirth.connect.client.core.api.util.OperationUtil;
+import com.mirth.connect.model.ExtensionPermission;
+import com.mirth.connect.plugins.ServicePlugin;
+import com.mirth.connect.plugins.dynamiclookup.server.cache.LookupCacheManager;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupAuditDao;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupGroupDao;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupStatisticsDao;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupValueDao;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.impl.MyBatisLookupAuditDao;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.impl.MyBatisLookupGroupDao;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.impl.MyBatisLookupStatisticsDao;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.impl.MyBatisLookupValueDao;
+import com.mirth.connect.plugins.dynamiclookup.server.service.LookupService;
+import com.mirth.connect.plugins.dynamiclookup.server.userutil.LookupHelper;
+import com.mirth.connect.plugins.dynamiclookup.server.util.SqlSessionManagerProvider;
+import com.mirth.connect.plugins.dynamiclookup.shared.interfaces.LookupTableServletInterface;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+import com.mirth.connect.server.util.DatabaseUtil;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionManager;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * @author Thai Tran (thaitran@innovarhealthcare.com)
+ * @create 2025-05-13 10:25 AM
+ */
+
+public class LookupTableServicePlugin implements ServicePlugin {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+ private final LookupService lookupService = LookupService.getInstance();
+ private LookupCacheManager cacheManager;
+
+ @Override
+ public void init(Properties properties) {
+ logger.info("Initializing Lookup Table Management System plugin...");
+ try {
+ // Initialize database if needed
+ initializeDatabase();
+
+ // Create DAO instances
+ SqlSessionManager sqlSessionManager = getSqlSessionManager();
+ LookupGroupDao groupDao = new MyBatisLookupGroupDao(sqlSessionManager);
+ LookupValueDao valueDao = new MyBatisLookupValueDao(sqlSessionManager);
+ LookupAuditDao auditDao = new MyBatisLookupAuditDao(sqlSessionManager);
+ LookupStatisticsDao statisticsDao = new MyBatisLookupStatisticsDao(sqlSessionManager);
+
+ // Create cache manager
+ cacheManager = new LookupCacheManager(groupDao);
+ // Init lookup service
+ lookupService.init(groupDao, valueDao, auditDao, statisticsDao, cacheManager);
+ // Initialize helper methods for transformers
+ LookupHelper.initialize(lookupService);
+ // Register transformer functions
+ registerScriptingFunctions();
+ logger.info("Lookup Table Management System plugin initialized successfully");
+ } catch (Exception e) {
+ logger.error("Error initializing Lookup Table Management System plugin", e);
+ throw new RuntimeException("Failed to initialize plugin: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void update(Properties properties) {
+
+ }
+
+ @Override
+ public Properties getDefaultProperties() {
+ return new Properties();
+ }
+
+ @Override
+ public ExtensionPermission[] getExtensionPermissions() {
+ ExtensionPermission viewPermission = new ExtensionPermission(
+ "Lookup Table Management System",
+ PERMISSION_ACCESS,
+ "Allows to accessing Lookup Table",
+ OperationUtil.getOperationNamesForPermission(PERMISSION_ACCESS, LookupTableServletInterface.class),
+ new String[]{}
+ );
+
+ return new ExtensionPermission[]{viewPermission};
+ }
+
+ @Override
+ public String getPluginPointName() {
+ return "Lookup Table Management System";
+ }
+
+ @Override
+ public void start() {
+ logger.info("Starting Lookup Table Management System plugin...");
+ try {
+ // Preload lookup tables for better performance
+ preloadLookupTables();
+ logger.info("Lookup Table Management System plugin started successfully");
+ } catch (Exception e) {
+ logger.error("Error starting Lookup Table Management System plugin", e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ logger.info("Stopping Lookup Table Management System plugin...");
+ try {
+ // Clear all caches
+ if (cacheManager != null) {
+ cacheManager.clearAllCaches();
+ }
+ logger.info("Lookup Table Management System plugin stopped successfully");
+ } catch (Exception e) {
+ logger.error("Error stopping Lookup Table Management System plugin", e);
+ }
+ }
+
+ /**
+ * Initialize database schema if needed
+ */
+ private void initializeDatabase() throws Exception {
+ logger.info("Initializing database schema...");
+ SqlSessionManager sqlSessionManager = getSqlSessionManager();
+ DatabaseType dbType = determineDatabaseType(sqlSessionManager);
+ SqlSession session = sqlSessionManager.openSession();
+ try {
+ // Check if tables already exist
+ if (!DatabaseUtil.tableExists(session.getConnection(), "LOOKUP_GROUP")) {
+ // Create tables based on database type
+ String migrationScript = getMigrationScript(dbType);
+ executeSqlScript(session, migrationScript);
+ logger.info("Database schema initialized successfully");
+ } else {
+ logger.info("Database schema already exists, skipping initialization");
+ }
+ } finally {
+ session.close();
+ }
+ }
+
+ /**
+ * Determine database type from connection
+ */
+ private DatabaseType determineDatabaseType(SqlSessionManager sqlSessionManager) throws SQLException {
+ SqlSession session = sqlSessionManager.openSession();
+
+ try {
+ Connection conn = session.getConnection();
+ String productName = conn.getMetaData().getDatabaseProductName().toLowerCase();
+ if (productName.contains("postgresql")) {
+ return DatabaseType.POSTGRESQL;
+ } else if (productName.contains("mysql")) {
+ return DatabaseType.MYSQL;
+ } else if (productName.contains("microsoft") || productName.contains("sql server")) {
+ return DatabaseType.SQLSERVER;
+ } else if (productName.contains("oracle")) {
+ return DatabaseType.ORACLE;
+ } else {
+ return DatabaseType.DERBY; // Default/fallback is Derby (Mirth's embedded DB)
+ }
+ } finally {
+ session.close();
+ }
+ }
+
+ /**
+ * Get migration script based on database type
+ */
+ private String getMigrationScript(DatabaseType dbType) {
+ // Load appropriate script based on database type
+ String scriptPath;
+ switch (dbType) {
+ case POSTGRESQL:
+ scriptPath = "/sql/postgres/create_lookup_tables.sql";
+ break;
+ case MYSQL:
+ scriptPath = "/sql/mysql/create_lookup_tables.sql";
+ break;
+ case SQLSERVER:
+ scriptPath = "/sql/sqlserver/create_lookup_tables.sql";
+ break;
+ case ORACLE:
+ scriptPath = "/sql/oracle/create_lookup_tables.sql";
+ break;
+ case DERBY:
+ default:
+ scriptPath = "/sql/derby/create_lookup_tables.sql";
+ break;
+ }
+ try (InputStream is = getClass().getResourceAsStream(scriptPath)) {
+ if (is == null) {
+ throw new RuntimeException("Failed to load migration script: script not found");
+ }
+
+ return IOUtils.toString(is, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load migration script: " + scriptPath, e);
+ }
+ }
+
+ /**
+ * Execute SQL script
+ */
+ private void executeSqlScript(SqlSession session, String script) {
+ String[] statements = script.split(";");
+
+ try {
+ Statement statement = session.getConnection().createStatement();
+
+ for (String statementString : statements) {
+ statementString = statementString.trim();
+ if (!statementString.isEmpty()) {
+ statement.execute(statementString);
+ }
+ }
+ session.commit();
+ } catch (Exception e) {
+ session.rollback();
+ throw new RuntimeException("Failed to execute SQL script: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Get SqlSessionManager from Mirth's context
+ */
+ private SqlSessionManager getSqlSessionManager() {
+ return SqlSessionManagerProvider.get();
+ }
+
+ /**
+ * Register JavaScript functions for transformers
+ */
+ private void registerScriptingFunctions() {
+ }
+
+ /**
+ * Preload frequently used lookup tables
+ */
+ private void preloadLookupTables() {
+ logger.info("Preloading lookup tables...");
+ try {
+ List groups = lookupService.getAllGroups();
+ int count = 0;
+ for (LookupGroup group : groups) {
+ if (group.getCacheSize() > 0) {
+ // Load values into cache
+ int valueCount = preloadGroupValues(group);
+ if (valueCount > 0) {
+ count++;
+ logger.info("Preloaded {} values for group: {} (ID: {})",
+ valueCount, group.getName(), group.getId());
+ }
+ }
+ }
+ logger.info("Completed preloading {} lookup groups", count);
+ } catch (Exception e) {
+ logger.error("Error preloading lookup tables", e);
+ }
+ }
+
+ /**
+ * Preload values for a specific group
+ */
+ private int preloadGroupValues(LookupGroup group) {
+ try {
+ int limit = group.getCacheSize() * 2;
+ List values = lookupService.searchLookupValues(group.getId(), 0, limit, null);
+
+ // Skip if the group is empty or too large for caching
+ if (values.isEmpty()) {
+ return 0;
+ }
+ // Load values into cache
+ for (LookupValue value : values) {
+ cacheManager.putValue(group.getId(), value.getKeyValue(), value.getValueData(), value.getUpdatedDate());
+ }
+ return values.size();
+ } catch (Exception e) {
+ logger.warn("Failed to preload values for group: {} (ID: {}): {}",
+ group.getName(), group.getId(), e.getMessage());
+ return 0;
+ }
+ }
+
+ /**
+ * Enum for supported database types
+ */
+ private enum DatabaseType {
+ DERBY, POSTGRESQL, MYSQL, SQLSERVER, ORACLE
+ }
+
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/service/LookupService.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/service/LookupService.java
new file mode 100644
index 000000000..64d5e54cc
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/service/LookupService.java
@@ -0,0 +1,859 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.service;
+
+import com.mirth.connect.plugins.dynamiclookup.server.cache.LookupCacheManager;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupAuditDao;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupGroupDao;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupStatisticsDao;
+import com.mirth.connect.plugins.dynamiclookup.server.dao.LookupValueDao;
+import com.mirth.connect.plugins.dynamiclookup.server.exception.DuplicateGroupNameException;
+import com.mirth.connect.plugins.dynamiclookup.server.exception.GroupNotFoundException;
+import com.mirth.connect.plugins.dynamiclookup.server.exception.ValueOperationException;
+import com.mirth.connect.plugins.dynamiclookup.server.exception.ValueTableCreationException;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.CacheStatistics;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.*;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.util.TtlUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.google.common.cache.CacheStats;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.ArrayList;
+
+public class LookupService {
+ private static LookupService instance = null;
+
+ private LookupGroupDao groupDao;
+ private LookupValueDao valueDao;
+ private LookupAuditDao auditDao;
+ private LookupStatisticsDao statisticsDao;
+ private LookupCacheManager cacheManager;
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ public static LookupService getInstance() {
+ synchronized (LookupService.class) {
+ if (instance == null) {
+ instance = new LookupService();
+ }
+ return instance;
+ }
+ }
+
+ // Constructor
+ public LookupService() {
+
+ }
+
+ // Constructor and dependency injection
+ public LookupService(
+ LookupGroupDao groupDao,
+ LookupValueDao valueDao,
+ LookupAuditDao auditDao,
+ LookupStatisticsDao statisticsDao,
+ LookupCacheManager cacheManager) {
+ this.groupDao = groupDao;
+ this.valueDao = valueDao;
+ this.auditDao = auditDao;
+ this.statisticsDao = statisticsDao;
+ this.cacheManager = cacheManager;
+ }
+
+ public void init(LookupGroupDao groupDao,
+ LookupValueDao valueDao,
+ LookupAuditDao auditDao,
+ LookupStatisticsDao statisticsDao,
+ LookupCacheManager cacheManager) {
+ this.groupDao = groupDao;
+ this.valueDao = valueDao;
+ this.auditDao = auditDao;
+ this.statisticsDao = statisticsDao;
+ this.cacheManager = cacheManager;
+ }
+
+ // Group Management
+
+ /**
+ * Retrieves all lookup groups
+ */
+ public List getAllGroups() {
+ return groupDao.getAllGroups();
+ }
+
+ /**
+ * Gets a specific group by ID
+ */
+ public LookupGroup getGroupById(int id) {
+ return groupDao.getGroupById(id);
+ }
+
+ /**
+ * Gets a specific group by name
+ */
+ public LookupGroup getGroupByName(String name) {
+ return groupDao.getGroupByName(name);
+ }
+
+ /**
+ * Creates a new lookup group and its corresponding value table
+ */
+ public int createGroup(LookupGroup group) {
+ // Validate group data
+ validateGroup(group);
+
+ // Check for duplicate name
+ if (groupDao.getGroupByName(group.getName()) != null) {
+ throw new DuplicateGroupNameException("Group name already exists: " + group.getName());
+ }
+
+ // Set defaults if not provided
+ if (group.getCacheSize() <= 0) {
+ group.setCacheSize(1000); // Default cache size
+ }
+ if (group.getCachePolicy() == null || group.getCachePolicy().isEmpty()) {
+ group.setCachePolicy("LRU"); // Default cache policy
+ }
+
+ // Insert group to get ID
+ int groupId = groupDao.insertGroup(group);
+
+ try {
+ // Create value table
+ String tableName = getTableNameForGroup(groupId);
+ groupDao.createValueTable(tableName);
+
+ // Initialize statistics
+ statisticsDao.insertStatistics(groupId);
+
+ logger.info("Created lookup group: {} (ID: {})", group.getName(), groupId);
+ return groupId;
+ } catch (Exception e) {
+ // If table creation fails, delete the group
+ try {
+ groupDao.deleteGroup(groupId);
+ } catch (Exception ex) {
+ logger.error("Failed to rollback group creation after table creation error", ex);
+ }
+ throw new ValueTableCreationException("Failed to create value table for group: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Updates an existing lookup group
+ */
+ public void updateGroup(LookupGroup group) {
+ // Validate group data
+ validateGroup(group);
+
+ // Check if group exists
+ LookupGroup existingGroup = groupDao.getGroupById(group.getId());
+ if (existingGroup == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + group.getId());
+ }
+
+ // Check if name changed and ensure it's still unique
+ if (!existingGroup.getName().equals(group.getName())) {
+ LookupGroup otherGroup = groupDao.getGroupByName(group.getName());
+ if (otherGroup != null && otherGroup.getId() != group.getId()) {
+ throw new DuplicateGroupNameException("Group name already exists: " + group.getName());
+ }
+ }
+
+ // Update group
+ groupDao.updateGroup(group);
+
+ // Clear cache if cache settings changed
+ if (existingGroup.getCacheSize() != group.getCacheSize() ||
+ !existingGroup.getCachePolicy().equals(group.getCachePolicy())) {
+ cacheManager.clearGroupCache(group.getId());
+ }
+
+ logger.info("Updated lookup group: {} (ID: {})", group.getName(), group.getId());
+ }
+
+ /**
+ * Deletes a lookup group and its value table
+ */
+ public void deleteGroup(int groupId) {
+ // Get group to verify it exists
+ LookupGroup group = groupDao.getGroupById(groupId);
+ if (group == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ String groupName = group.getName();
+
+ // Drop value table
+ String tableName = getTableNameForGroup(groupId);
+ groupDao.dropValueTable(tableName);
+
+ // Delete group metadata
+ groupDao.deleteGroup(groupId);
+
+ // Clear cache
+ cacheManager.clearGroupCache(groupId);
+
+ logger.info("Deleted lookup group: {} (ID: {})", groupName, groupId);
+ }
+
+ public int importGroup(LookupGroup group, Map values, boolean updateIfExists, String userId) {
+ try {
+ //Check if group already exists
+ LookupGroup existing = groupDao.getGroupByName(group.getName());
+ int groupId;
+ String tableName;
+
+ if (existing != null) {
+ if (!updateIfExists) {
+ throw new DuplicateGroupNameException("Group name already exists: " + group.getName());
+ }
+
+ // Update existing group metadata
+ groupId = existing.getId();
+ group.setId(groupId);
+
+ updateGroup(group);
+
+ // Clear existing values before inserting new ones
+ tableName = getTableNameForGroup(groupId);
+
+ // Get count before deletion for audit
+ long count = valueDao.getValueCount(tableName);
+
+ // Delete all values
+ valueDao.deleteAllValues(tableName);
+
+ // Audit
+ recordAudit(groupId, tableName, "*", "DELETE_ALL", count + " values", null, userId);
+
+ // Clear cache for this group
+ cacheManager.clearGroupCache(groupId);
+ } else {
+ // Insert new group and get generated ID
+ groupId = createGroup(group);
+ group.setId(groupId);
+ tableName = getTableNameForGroup(groupId);
+ }
+
+ // Import all values
+ int count = valueDao.importValues(tableName, values);
+
+ // Audit
+ recordAudit(groupId, tableName, "*", "IMPORT", null, count + " values imported", userId);
+
+ logger.info("Imported {} values into group: {} (ID: {})", count, group.getName(), groupId);
+
+ return count;
+ } catch (DuplicateGroupNameException e) {
+ throw e;
+ } catch (Exception e) {
+ logger.error("Failed to import lookup group: " + e.getMessage(), e);
+ throw new ValueOperationException("Failed to import lookup group: " + e.getMessage(), e);
+ }
+ }
+
+ // Value Management
+
+ /**
+ * Retrieves a raw lookup value (including its metadata) directly from the database.
+ *
+ * This method bypasses the cache and is typically used by administrative tools
+ * or the UI to display the current value and its update metadata.
+ *
+ *
+ * @param groupId the ID of the lookup group
+ * @param key the lookup key
+ * @return a {@link LookupValue} containing the value and updated timestamp,
+ * or {@code null} if the key is not found in the specified group
+ * @throws GroupNotFoundException if the group does not exist
+ */
+ public LookupValue getLookupValue(int groupId, String key) {
+ validateKey(key);
+
+ // Verify group exists
+ if (groupDao.getGroupById(groupId) == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ String tableName = getTableNameForGroup(groupId);
+ return valueDao.getLookupValue(tableName, key);
+ }
+
+ /**
+ * Retrieves a value from a lookup group without applying TTL validation.
+ *
+ * This method always returns the value if found, regardless of how old it is.
+ * It is equivalent to calling {@code getValue(groupId, key, 0)}.
+ *
+ *
+ * @param groupId the lookup group ID
+ * @param key the lookup key
+ * @return the value if found in cache or database, otherwise null
+ */
+ public String getValue(int groupId, String key) {
+ return getValue(groupId, key, 0);
+ }
+
+ /**
+ * Retrieves a value from a lookup group using the following flow:
+ *
+ *
Attempt to retrieve from cache.
+ *
If not present or stale (based on TTL), load from the database.
+ *
If loaded from the database, cache the value (along with its updatedAt timestamp).
+ *
If the database value is also stale (based on TTL), return null.
+ *
+ *
+ * @param groupId the lookup group ID
+ * @param key the lookup key
+ * @param ttlHours if > 0, the cached value must have an updatedAt within this TTL
+ * @return the value if found and valid, otherwise null
+ */
+ public String getValue(int groupId, String key, long ttlHours) {
+ validateKey(key);
+
+ // Verify group exists
+ if (groupDao.getGroupById(groupId) == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ // Try to get from cache first
+ String value = cacheManager.getValue(groupId, key, ttlHours);
+ boolean cacheHit = (value != null);
+
+ if (!cacheHit) {
+ // Not in cache, get from database
+ String tableName = getTableNameForGroup(groupId);
+ LookupValue lookupValue = valueDao.getLookupValue(tableName, key);
+
+ if (lookupValue != null) {
+ value = lookupValue.getValueData();
+ Date updatedAt = lookupValue.getUpdatedDate();
+
+ if (value != null && updatedAt != null) {
+ // Add to cache
+ cacheManager.putValue(groupId, key, value, updatedAt);
+
+ // Re-validate against TTL
+ if (!TtlUtils.isWithinTtl(updatedAt, ttlHours)) {
+ value = null; // Reject if data is stale
+ }
+ }
+ }
+ }
+
+ // Update statistics
+ try {
+ statisticsDao.updateStatistics(groupId, cacheHit);
+ } catch (Exception e) {
+ // Non-critical error, just log it
+ logger.warn("Failed to update statistics: {}", e.getMessage());
+ }
+
+ return value;
+ }
+
+ /**
+ * Retrieves all values from a lookup group
+ */
+ public Map getAllValues(int groupId) {
+ // Verify group exists
+ if (groupDao.getGroupById(groupId) == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ String tableName = getTableNameForGroup(groupId);
+
+ List values = valueDao.getAllValues(tableName);
+ Map map = new LinkedHashMap<>();
+ for (LookupValue value : values) {
+ map.put(value.getKeyValue(), value.getValueData());
+ }
+
+ return map;
+ }
+
+ /**
+ * Returns the total count of lookup values in the specified group
+ * that match the given search pattern on key or value fields.
+ */
+ public int searchLookupValuesCount(int groupId, String pattern) {
+ if (groupDao.getGroupById(groupId) == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ String tableName = getTableNameForGroup(groupId);
+
+ long count = valueDao.searchLookupValuesCount(tableName, pattern);
+
+ return Math.toIntExact(count);
+ }
+
+ /**
+ * Retrieves key-value pairs from the specified lookup group that match the given pattern.
+ */
+ public List searchLookupValues(Integer groupId, Integer offset, Integer limit, String pattern) {
+ // Verify group exists
+ if (groupDao.getGroupById(groupId) == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ String tableName = getTableNameForGroup(groupId);
+
+ return valueDao.searchLookupValues(tableName, offset, limit, pattern);
+ }
+
+ /**
+ * Retrieves values matching a pattern from a lookup group
+ */
+ public Map getMatchingValues(int groupId, String keyPattern) {
+ // Verify group exists
+ if (groupDao.getGroupById(groupId) == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ String tableName = getTableNameForGroup(groupId);
+ List values = valueDao.getMatchingValues(tableName, keyPattern);
+
+ Map map = new LinkedHashMap<>();
+ for (LookupValue value : values) {
+ map.put(value.getKeyValue(), value.getValueData());
+ }
+
+ return map;
+ }
+
+ /**
+ * Retrieves multiple values from a lookup group without applying TTL validation.
+ *
+ * This method checks the cache first. Any missing values are retrieved from the database
+ * and added to the cache. Values are returned regardless of how old they are.
+ *
+ *
+ * @param groupId the lookup group ID
+ * @param keys the list of lookup keys to retrieve
+ * @return a map of keys to values for those that were found
+ * @throws GroupNotFoundException if the group does not exist
+ */
+ public Map getBatchValues(int groupId, List keys) {
+ return getBatchValues(groupId, keys, 0);
+ }
+
+
+ /**
+ * Retrieves multiple values from a lookup group, applying optional TTL validation.
+ *
+ * Lookup flow for each key:
+ *
+ *
Try to retrieve from cache.
+ *
If not found or stale (based on TTL), load from the database and cache it.
+ *
If the database value is also stale, it is excluded from the result.
+ *
+ * TTL is based on the value's {@code updatedAt} timestamp.
+ *
+ *
+ * @param groupId the lookup group ID
+ * @param keys the list of lookup keys to retrieve
+ * @param ttlHours time-to-live in hours; if 0 or less, TTL is ignored
+ * @return a map of keys to values that passed TTL validation (if applicable)
+ * @throws GroupNotFoundException if the group does not exist
+ */
+ public Map getBatchValues(int groupId, List keys, long ttlHours) {
+ // Verify group exists
+ if (groupDao.getGroupById(groupId) == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ Map result = new HashMap<>();
+ String tableName = getTableNameForGroup(groupId);
+
+ // First check cache for all keys
+ int cacheHits = 0;
+ List keysToFetch = new ArrayList<>();
+
+ for (String key : keys) {
+ String value = cacheManager.getValue(groupId, key, ttlHours);
+ if (value != null) {
+ result.put(key, value);
+ cacheHits++;
+ } else {
+ keysToFetch.add(key);
+ }
+ }
+
+ // Fetch any keys not found in cache
+ if (!keysToFetch.isEmpty()) {
+ for (String key : keysToFetch) {
+ LookupValue lookupValue = valueDao.getLookupValue(tableName, key);
+ if (lookupValue != null) {
+ String value = lookupValue.getValueData();
+ Date updatedAt = lookupValue.getUpdatedDate();
+
+ if (value != null && updatedAt != null) {
+ cacheManager.putValue(groupId, key, value, updatedAt);
+
+ if (TtlUtils.isWithinTtl(updatedAt, ttlHours)) {
+ result.put(key, value);
+ } else {
+ logger.debug("DB value for key '{}' excluded due to TTL (updatedAt={}, ttlHours={})", key, updatedAt, ttlHours);
+ }
+ }
+ }
+ }
+ }
+
+ // Update statistics
+ try {
+ for (int i = 0; i < cacheHits; i++) {
+ statisticsDao.updateStatistics(groupId, true);
+ }
+ for (int i = 0; i < keysToFetch.size(); i++) {
+ statisticsDao.updateStatistics(groupId, false);
+ }
+ } catch (Exception e) {
+ // Non-critical error, just log it
+ logger.warn("Failed to update statistics: {}", e.getMessage());
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates or updates a value in a lookup group
+ */
+ public void setValue(int groupId, String key, String value, String userId) {
+ // Validate inputs
+ validateKey(key);
+ if (value == null) {
+ throw new IllegalArgumentException("Value cannot be null");
+ }
+
+ // Verify group exists
+ LookupGroup group = groupDao.getGroupById(groupId);
+ if (group == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ String tableName = getTableNameForGroup(groupId);
+ String existingValue = valueDao.getValue(tableName, key);
+
+ try {
+ if (existingValue == null) {
+ // Insert new value
+ valueDao.insertValue(tableName, key, value);
+
+ // Audit
+ recordAudit(groupId, tableName, key, "CREATE", null, value, userId);
+
+ logger.debug("Created lookup value - Group: {}, Key: {}", group.getName(), key);
+ } else {
+ // Update existing value
+ valueDao.updateValue(tableName, key, value);
+
+ // Audit
+ recordAudit(groupId, tableName, key, "UPDATE", existingValue, value, userId);
+
+ logger.debug("Updated lookup value - Group: {}, Key: {}", group.getName(), key);
+ }
+
+ // Update cache
+ LookupValue lookupValue = valueDao.getLookupValue(tableName, key);
+ cacheManager.putValue(groupId, key, value, lookupValue.getUpdatedDate());
+ } catch (Exception e) {
+ throw new ValueOperationException("Failed to set value: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Deletes a value from a lookup group
+ */
+ public void deleteValue(int groupId, String key, String userId) {
+ validateKey(key);
+
+ // Verify group exists
+ LookupGroup group = groupDao.getGroupById(groupId);
+ if (group == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ String tableName = getTableNameForGroup(groupId);
+ String existingValue = valueDao.getValue(tableName, key);
+
+ if (existingValue != null) {
+ try {
+ // Delete value
+ valueDao.deleteValue(tableName, key);
+
+ // Audit
+ recordAudit(groupId, tableName, key, "DELETE", existingValue, null, userId);
+
+ // Remove from cache
+ cacheManager.removeValue(groupId, key);
+
+ logger.debug("Deleted lookup value - Group: {}, Key: {}", group.getName(), key);
+ } catch (Exception e) {
+ throw new ValueOperationException("Failed to delete value: " + e.getMessage(), e);
+ }
+ } else {
+ logger.debug("No value found to delete - Group: {}, Key: {}", group.getName(), key);
+ }
+ }
+
+ /**
+ * Deletes all values from a lookup group
+ */
+ public void deleteAllValues(int groupId, String userId) {
+ // Verify group exists
+ LookupGroup group = groupDao.getGroupById(groupId);
+ if (group == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ String tableName = getTableNameForGroup(groupId);
+
+ try {
+ // Get count before deletion for audit
+ long count = valueDao.getValueCount(tableName);
+
+ // Delete all values
+ valueDao.deleteAllValues(tableName);
+
+ // Audit
+ recordAudit(groupId, tableName, "*", "DELETE_ALL", count + " values", null, userId);
+
+ // Clear cache
+ cacheManager.clearGroupCache(groupId);
+
+ logger.info("Deleted all values from group: {} (ID: {}), count: {}", group.getName(), groupId, count);
+ } catch (Exception e) {
+ throw new ValueOperationException("Failed to delete all values: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Imports values into a lookup group
+ */
+ public int importValues(int groupId, Map values, boolean clearExisting, String userId) {
+ // Verify group exists
+ LookupGroup group = groupDao.getGroupById(groupId);
+ if (group == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ if (values == null || values.isEmpty()) {
+ return 0;
+ }
+
+ String tableName = getTableNameForGroup(groupId);
+
+ try {
+ // Clear existing values if requested
+ if (clearExisting) {
+ valueDao.deleteAllValues(tableName);
+ // Audit the clearing operation
+ recordAudit(groupId, tableName, "*", "CLEAR_ALL", "Before import", null, userId);
+ }
+
+ // Import all values
+ int count = valueDao.importValues(tableName, values);
+
+ // Audit
+ recordAudit(groupId, tableName, "*", "IMPORT", null, count + " values imported", userId);
+
+ // Clear cache for this group
+ cacheManager.clearGroupCache(groupId);
+
+ logger.info("Imported {} values into group: {} (ID: {})", count, group.getName(), groupId);
+ return count;
+ } catch (Exception e) {
+ throw new ValueOperationException("Failed to import values: " + e.getMessage(), e);
+ }
+ }
+
+ // Statistics and Monitoring
+
+ /**
+ * Gets statistics for a lookup group
+ */
+ public LookupStatistics getStatistics(int groupId) {
+ // Verify group exists
+ if (groupDao.getGroupById(groupId) == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ return statisticsDao.getStatistics(groupId);
+ }
+
+ /**
+ * Returns a complete summary of cache metrics for the given group,
+ * including Guava stats, current entry count, and configured size limit.
+ */
+ public CacheStatistics getCacheStatistics(int groupId) {
+ LookupGroup group = groupDao.getGroupById(groupId);
+ if (group == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ CacheStats stats = cacheManager.getCacheStats(groupId);
+ boolean supported = (stats != null);
+
+ return new CacheStatistics(
+ supported,
+ group.getCachePolicy(),
+ cacheManager.getCacheSize(groupId),
+ group.getCacheSize(),
+ supported ? stats.hitCount() : 0,
+ supported ? stats.missCount() : 0,
+ supported ? stats.loadSuccessCount() : 0,
+ supported ? stats.loadExceptionCount() : 0,
+ supported ? stats.totalLoadTime() : 0,
+ supported ? stats.evictionCount() : 0
+ );
+ }
+
+ /**
+ * Resets statistics for a lookup group
+ */
+ public void resetStatistics(int groupId) {
+ // Verify group exists
+ if (groupDao.getGroupById(groupId) == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ statisticsDao.resetStatistics(groupId);
+ logger.info("Reset statistics for group ID: {}", groupId);
+ }
+
+ /**
+ * Gets audit entries for a lookup group
+ */
+ public List getAuditEntries(int groupId, int offset, int limit) {
+ return auditDao.getAuditEntriesByGroup(groupId, offset, limit);
+ }
+
+ /**
+ * Search audit entries for a lookup group
+ */
+ public List searchAuditEntries(int groupId, int offset, int limit, HistoryFilterState filter) {
+ return auditDao.searchAuditEntriesByGroup(groupId, offset, limit, filter);
+ }
+
+ /**
+ * Count audit entries for a lookup group
+ */
+ public int getAuditEntryCount(int groupId) {
+ long count = auditDao.getAuditEntryCount(groupId);
+
+ return Math.toIntExact(count);
+ }
+
+ /**
+ * Count audit entries for a lookup group
+ */
+ public int searchAuditEntryCount(int groupId, HistoryFilterState filter) {
+ long count = auditDao.searchAuditEntryCount(groupId, filter);
+
+ return Math.toIntExact(count);
+ }
+
+ /**
+ * Clears the in-memory cache for a specific lookup group.
+ */
+ public void clearGroupCache(int groupId) {
+ // Verify group exists
+ LookupGroup group = groupDao.getGroupById(groupId);
+ if (group == null) {
+ throw new GroupNotFoundException("Group not found with ID: " + groupId);
+ }
+
+ cacheManager.clearGroupCache(groupId);
+ }
+
+ /**
+ * Clears the in-memory caches for all lookup groups.
+ */
+ public void clearAllCaches() {
+ cacheManager.clearAllCaches();
+ }
+
+ // Helper methods
+
+ /**
+ * Constructs the table name for a group
+ */
+ private String getTableNameForGroup(int groupId) {
+ return "LOOKUP_VALUE_" + groupId;
+ }
+
+ /**
+ * Records an audit entry
+ */
+ private void recordAudit(int groupId, String tableName, String key, String action,
+ String oldValue, String newValue, String userId) {
+ try {
+ LookupAudit audit = new LookupAudit();
+ audit.setGroupId(groupId);
+ audit.setTableName(tableName);
+ audit.setKeyValue(key);
+ audit.setAction(action);
+ audit.setOldValue(oldValue);
+ audit.setNewValue(newValue);
+ audit.setUserId(userId);
+
+ auditDao.insertAuditEntry(audit);
+ } catch (Exception e) {
+ // Non-critical error, just log it
+ logger.warn("Failed to record audit entry: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * Validates a lookup group
+ */
+ private void validateGroup(LookupGroup group) {
+ if (group == null) {
+ throw new IllegalArgumentException("Group cannot be null");
+ }
+
+ if (group.getName() == null || group.getName().trim().isEmpty()) {
+ throw new IllegalArgumentException("Group name cannot be empty");
+ }
+
+ if (group.getName().length() > 255) {
+ throw new IllegalArgumentException("Group name cannot exceed 255 characters");
+ }
+
+ // Validate cache policy
+ String policy = group.getCachePolicy();
+ if (policy != null && !policy.isEmpty() &&
+ !policy.equalsIgnoreCase("LRU") && !policy.equalsIgnoreCase("FIFO")) {
+ throw new IllegalArgumentException("Invalid cache policy: " + policy + ". Must be LRU or FIFO");
+ }
+ }
+
+ /**
+ * Validates a key
+ */
+ private void validateKey(String key) {
+ if (key == null || key.trim().isEmpty()) {
+ throw new IllegalArgumentException("Key cannot be empty");
+ }
+
+ if (key.length() > 255) {
+ throw new IllegalArgumentException("Key cannot exceed 255 characters");
+ }
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/servlet/LookupTableServlet.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/servlet/LookupTableServlet.java
new file mode 100644
index 000000000..543d4d919
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/servlet/LookupTableServlet.java
@@ -0,0 +1,637 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.servlet;
+
+import com.mirth.connect.client.core.api.MirthApiException;
+import com.mirth.connect.model.User;
+import com.mirth.connect.plugins.dynamiclookup.server.exception.DuplicateGroupNameException;
+import com.mirth.connect.plugins.dynamiclookup.server.exception.GroupNotFoundException;
+import com.mirth.connect.plugins.dynamiclookup.server.exception.LookupApiException;
+import com.mirth.connect.plugins.dynamiclookup.server.service.LookupService;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.LookupModelMapper;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.request.*;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.*;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.*;
+import com.mirth.connect.plugins.dynamiclookup.shared.interfaces.LookupTableServletInterface;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.util.JsonUtils;
+import com.mirth.connect.plugins.dynamiclookup.shared.util.LookupErrorCode;
+import com.mirth.connect.server.api.MirthServlet;
+import com.mirth.connect.client.core.ClientException;
+
+import com.mirth.connect.server.controllers.ControllerFactory;
+import com.mirth.connect.server.controllers.UserController;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+
+import java.util.Map;
+import java.util.Date;
+import java.util.List;
+import java.util.Collections;
+import java.util.HashMap;
+
+import java.util.stream.Collectors;
+
+public class LookupTableServlet extends MirthServlet implements LookupTableServletInterface {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ private static final UserController userController = ControllerFactory.getFactory().createUserController();
+
+ public LookupTableServlet(@Context HttpServletRequest request, @Context SecurityContext sc) {
+ super(request, sc, "Lookup Table Management System");
+ }
+
+ @Override
+ public String getAllGroups() throws ClientException {
+ try {
+ List groups = LookupService.getInstance().getAllGroups();
+
+ return JsonUtils.toJson(groups);
+ } catch (Exception e) {
+ // Wrap and rethrow any exception as a client-facing exception
+ // Could be validation, parsing, DB, or serialization errors
+ throw new ClientException("Failed to process getAllGroups request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String getGroupById(Integer groupId) throws ClientException {
+ try {
+ LookupGroup group = LookupService.getInstance().getGroupById(groupId);
+
+ if (group == null) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ }
+
+ return JsonUtils.toJson(group);
+
+ } catch (LookupApiException e) {
+ // Rethrow directly if it's already our custom API exception
+ throw e;
+
+ } catch (Exception e) {
+ // Catch-all for unexpected internal errors
+ throw new ClientException("Failed to process getGroupById request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String getGroupByName(String name) throws ClientException {
+ try {
+ if (name == null || name.trim().isEmpty()) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Group name must not be empty.");
+ }
+
+ LookupGroup group = LookupService.getInstance().getGroupByName(name);
+
+ if (group == null) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with name: " + name);
+ }
+
+ return JsonUtils.toJson(group);
+ } catch (LookupApiException e) {
+ // Rethrow directly if it's already our custom API exception
+ throw e;
+ } catch (Exception e) {
+ throw new ClientException("Failed to process getGroupByName request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String createGroup(String requestBody) throws ClientException {
+ try {
+ // Step 1: Parse incoming JSON string into a request DTO
+ LookupGroupRequest request;
+ try {
+ request = JsonUtils.fromJson(requestBody, LookupGroupRequest.class);
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Invalid JSON format for group request: " + e.getMessage());
+ }
+
+ // Step 2: Validate required fields in the input
+ try {
+ request.validate();
+ } catch (IllegalArgumentException e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Validation failed: " + e.getMessage());
+ }
+
+ // Step 3: Map DTO to domain object
+ LookupGroup group = LookupModelMapper.fromGroupDto(request);
+
+ // Step 4: Try to create the group
+ int groupId;
+ try {
+ groupId = LookupService.getInstance().createGroup(group);
+ } catch (DuplicateGroupNameException e) {
+ throw new LookupApiException(Response.Status.CONFLICT, LookupErrorCode.DUPLICATE_GROUP_NAME, "Group name already exists: " + group.getName());
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.INTERNAL_SERVER_ERROR, LookupErrorCode.DATABASE_ERROR, "Database error while creating group: " + e.getMessage());
+ }
+
+ // Step 5: Load full persisted group for response
+ LookupGroup fullGroup = LookupService.getInstance().getGroupById(groupId);
+ if (fullGroup == null) {
+ throw new LookupApiException(Response.Status.INTERNAL_SERVER_ERROR, LookupErrorCode.GROUP_NOT_FOUND, "Group created but could not be retrieved with ID: " + groupId);
+ }
+
+ // Step 6: Return JSON
+ return JsonUtils.toJson(fullGroup);
+ } catch (LookupApiException e) {
+ // Rethrow cleanly to let the Web API layer handle it
+ throw e;
+ } catch (Exception e) {
+ throw new ClientException("Failed to process createGroup request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String updateGroup(Integer groupId, String requestBody) throws ClientException {
+ try {
+ // Step 1: Parse incoming JSON string into a request DTO
+ LookupGroupRequest request;
+ try {
+ request = JsonUtils.fromJson(requestBody, LookupGroupRequest.class);
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Invalid JSON format for group request: " + e.getMessage());
+ }
+
+ // Step 2: Validate required fields in the input
+ try {
+ request.validate();
+ } catch (IllegalArgumentException e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Validation failed: " + e.getMessage());
+ }
+
+ // Step 3: Map DTO to domain object
+ LookupGroup group = LookupModelMapper.fromGroupDto(request);
+ group.setId(groupId);
+
+ // Step 4: Try to update the group
+ try {
+ LookupService.getInstance().updateGroup(group);
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (DuplicateGroupNameException e) {
+ throw new LookupApiException(Response.Status.CONFLICT, LookupErrorCode.DUPLICATE_GROUP_NAME, "Group name already exists: " + group.getName());
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.INTERNAL_SERVER_ERROR, LookupErrorCode.DATABASE_ERROR, "Database error while updating group: " + e.getMessage());
+ }
+
+ // Step 5: Load full persisted group for response
+ LookupGroup fullGroup = LookupService.getInstance().getGroupById(groupId);
+ if (fullGroup == null) {
+ throw new LookupApiException(Response.Status.INTERNAL_SERVER_ERROR, LookupErrorCode.GROUP_NOT_FOUND, "Group created but could not be retrieved with ID: " + groupId);
+ }
+
+ // Step 6: Return JSON
+ return JsonUtils.toJson(fullGroup);
+ } catch (LookupApiException e) {
+ // Rethrow cleanly to let the Web API layer handle it
+ throw e;
+ } catch (Exception e) {
+ throw new ClientException("Failed to process updateGroup request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public void deleteGroup(Integer groupId) throws ClientException {
+ try {
+ LookupService.getInstance().deleteGroup(groupId);
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ }
+ }
+
+ @Override
+ public String exportGroup(Integer groupId) throws ClientException {
+ try {
+ Map values = LookupService.getInstance().getAllValues(groupId);
+
+ LookupGroup group = LookupService.getInstance().getGroupById(groupId);
+ Date now = new Date(); // current UTC timestamp
+
+ ExportLookupGroupResponse response = new ExportLookupGroupResponse(group, values, now);
+
+ return JsonUtils.toJson(response);
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.INTERNAL_SERVER_ERROR, LookupErrorCode.DATABASE_ERROR, "Database error while creating group: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public String exportGroupPaged(Integer groupId, Integer offset, Integer limit) throws ClientException {
+ try {
+ // Validate group first
+ LookupGroup lookupGroup = LookupService.getInstance().getGroupById(groupId);
+ if (lookupGroup == null) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ }
+
+ // Normalize pagination and pattern
+ int safeOffset = offset != null ? offset : 0;
+ int safeLimit = limit != null ? limit : 10000;
+
+ List paginated = LookupService.getInstance().searchLookupValues(groupId, safeOffset, safeLimit, null);
+ int totalCount = LookupService.getInstance().searchLookupValuesCount(groupId, null);
+
+ // create response
+ ExportGroupPagedResponse response = ExportGroupPagedResponse.fromResult(groupId, safeOffset, safeLimit, totalCount, paginated);
+
+ return JsonUtils.toJson(response);
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (LookupApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ClientException("Failed to process exportGroupPaged request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String importGroup(boolean updateIfExists, String requestBody) throws ClientException {
+ try {
+ // Step 1: Parse and validate JSON input
+ ImportLookupGroupRequest request;
+ try {
+ request = JsonUtils.fromJson(requestBody, ImportLookupGroupRequest.class);
+ request.validate();
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Invalid import request: " + e.getMessage());
+ }
+
+ // Step 2: Import logic
+ LookupGroup group = request.getGroup();
+ Map values = request.getValues();
+
+ // Step 3: get current user id
+ String userId = String.valueOf(getCurrentUserId());
+
+ // Delegating to service
+ int count;
+ try {
+ count = LookupService.getInstance().importGroup(group, values, updateIfExists, userId);
+ } catch (DuplicateGroupNameException e) {
+ throw new LookupApiException(Response.Status.CONFLICT, LookupErrorCode.DUPLICATE_GROUP_NAME, "Group name already exists: " + group.getName());
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.INTERNAL_SERVER_ERROR, LookupErrorCode.DATABASE_ERROR, "Database error while import group: " + e.getMessage());
+ }
+
+ // Step 4: Build response DTO
+ ImportLookupGroupResponse response = ImportLookupGroupResponse.fromResult(group.getId(), count, Collections.emptyList());
+
+ return JsonUtils.toJson(response);
+ } catch (LookupApiException e) {
+ // Rethrow cleanly to let the Web API layer handle it
+ throw e;
+ } catch (Exception e) {
+ throw new ClientException("Failed to import lookup group: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String getAllValues(Integer groupId, Integer offset, Integer limit, String pattern) throws ClientException {
+ try {
+ // Validate group first
+ LookupGroup lookupGroup = LookupService.getInstance().getGroupById(groupId);
+ if (lookupGroup == null) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ }
+
+ // Normalize pagination and pattern
+ int safeOffset = offset != null ? offset : 0;
+ int safeLimit = limit != null ? limit : Integer.MAX_VALUE;
+ String safePattern = (pattern != null) ? pattern.trim() : null;
+
+ List paginated = LookupService.getInstance().searchLookupValues(groupId, safeOffset, safeLimit, safePattern);
+ int totalCount = LookupService.getInstance().searchLookupValuesCount(groupId, safePattern);
+
+ LookupAllValuesResponse response = LookupAllValuesResponse.fromResult(
+ groupId,
+ lookupGroup.getName(),
+ totalCount,
+ paginated,
+ safeLimit,
+ safeOffset
+ );
+
+ return JsonUtils.toJson(response);
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (LookupApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ClientException("Failed to process getAllValues request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String getValue(Integer groupId, String key) throws ClientException {
+ try {
+ // Step 1: Return JSON
+ String value = LookupService.getInstance().getValue(groupId, key);
+
+ if (value == null) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.VALUE_NOT_FOUND, "Lookup value not found with key: " + key);
+ }
+
+ LookupValue lookupValue = new LookupValue(key, value);
+
+ // Step 2: Return JSON
+ return JsonUtils.toJson(LookupModelMapper.toValueResponse(lookupValue, groupId, false));
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (LookupApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ClientException("Failed to process getValue request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String setValue(Integer groupId, String key, String requestBody) {
+ try {
+ // Step 1: Parse incoming JSON string into a request DTO
+ LookupValueRequest request;
+ try {
+ request = JsonUtils.fromJson(requestBody, LookupValueRequest.class);
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Invalid JSON format for value request: " + e.getMessage());
+ }
+
+ // Step 2: Validate required fields in the input
+ try {
+ request.validate();
+ } catch (IllegalArgumentException e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Validation failed: " + e.getMessage());
+ }
+
+ // Step 3: get current user id
+ String userId = String.valueOf(getCurrentUserId());
+
+ // Step 4: Try to set value
+ try {
+ LookupService.getInstance().setValue(groupId, key, request.getValue(), userId);
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.INTERNAL_SERVER_ERROR, LookupErrorCode.DATABASE_ERROR, "Database error while set value: " + e.getMessage());
+ }
+
+ // Step 5: Load full persisted value for response
+ LookupValue lookupValue = LookupService.getInstance().getLookupValue(groupId, key);
+ if (lookupValue == null) {
+ throw new LookupApiException(Response.Status.INTERNAL_SERVER_ERROR, LookupErrorCode.GROUP_NOT_FOUND, "The value was saved, but failed to retrieve it afterward. groupId=" + groupId + ", key=" + key);
+ }
+
+ // Step 6: Return JSON
+ return JsonUtils.toJson(LookupModelMapper.toValueResponse(lookupValue, groupId));
+
+ } catch (LookupApiException e) {
+ throw e;
+ } catch (Exception e) {
+// throw new ClientException("Failed to process setValue request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ throw new MirthApiException(e);
+ }
+ }
+
+ @Override
+ public void deleteValue(Integer groupId, String key) throws ClientException {
+ try {
+ String userId = String.valueOf(getCurrentUserId());
+ LookupService.getInstance().deleteValue(groupId, key, userId);
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ }
+ }
+
+ @Override
+ public String importValues(Integer groupId, boolean clearExisting, String requestBody) {
+ // Step 1: Parse incoming JSON string into a request DTO
+ ImportValuesRequest request;
+ try {
+ request = JsonUtils.fromJson(requestBody, ImportValuesRequest.class);
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Invalid JSON format for import values request: " + e.getMessage());
+ }
+
+ // Step 2: Validate required fields in the input
+ try {
+ request.validate();
+ } catch (IllegalArgumentException e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Validation failed: " + e.getMessage());
+ }
+
+ // Step 3: Map DTO to domain object
+ Map values = LookupModelMapper.fromImportValuesDto(request);
+
+ // Step 4: Try to create the group
+ String userId = String.valueOf(getCurrentUserId());
+ try {
+ int importedCount = LookupService.getInstance().importValues(groupId, values, clearExisting, userId);
+
+ ImportValuesResponse response = new ImportValuesResponse();
+ response.setGroupId(groupId);
+ response.setStatus("success");
+ response.setImportedCount(importedCount);
+ response.setErrors(Collections.emptyList());
+
+ return JsonUtils.toJson(response);
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.INTERNAL_SERVER_ERROR, LookupErrorCode.DATABASE_ERROR, "Database error while importing values: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public String batchGetValues(Integer groupId, String requestBody) throws ClientException {
+ // Step 1: Parse incoming JSON string into a request DTO
+ BatchGetValuesRequest request;
+ try {
+ request = JsonUtils.fromJson(requestBody, BatchGetValuesRequest.class);
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Invalid JSON format for import values request: " + e.getMessage());
+ }
+
+ // Step 2: Validate required fields in the input
+ try {
+ request.validate();
+ } catch (IllegalArgumentException e) {
+ throw new LookupApiException(Response.Status.BAD_REQUEST, LookupErrorCode.INVALID_REQUEST, "Validation failed: " + e.getMessage());
+ }
+
+ // Step 3: Map DTO to domain object
+ List keys = LookupModelMapper.fromBatchGetValues(request);
+
+ // Step 4: Try to create the group
+ try {
+ Map map = LookupService.getInstance().getBatchValues(groupId, keys);
+
+ BatchGetValuesResponse response = new BatchGetValuesResponse();
+ response.setGroupId(groupId);
+ response.setValues(map);
+
+ List missingKeys = keys.stream()
+ .filter(k -> !map.containsKey(k))
+ .collect(Collectors.toList());
+
+ response.setMissingKeys(missingKeys);
+
+ return JsonUtils.toJson(response);
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (Exception e) {
+ throw new LookupApiException(Response.Status.INTERNAL_SERVER_ERROR, LookupErrorCode.DATABASE_ERROR, "Database error while creating group: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public String getGroupStatistics(Integer groupId) throws ClientException {
+ try {
+ LookupStatistics dbStats = LookupService.getInstance().getStatistics(groupId);
+ CacheStatistics cacheStatistics = LookupService.getInstance().getCacheStatistics(groupId);
+
+ GroupStatisticsResponse response = GroupStatisticsResponse.fromResult(dbStats, cacheStatistics);
+
+ return JsonUtils.toJson(response);
+
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (Exception e) {
+ // Catch-all for unexpected internal errors
+ throw new ClientException("Failed to process getGroupStatistics request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String resetGroupStatistics(Integer groupId) throws ClientException {
+ try {
+ LookupService.getInstance().resetStatistics(groupId);
+
+ Map response = new HashMap<>();
+ response.put("status", "success");
+ response.put("message", "Statistics reset successfully for group ID: " + groupId);
+
+ return JsonUtils.toJson(response);
+
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (Exception e) {
+ // Catch-all for unexpected internal errors
+ throw new ClientException("Failed to process getGroupStatistics request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String getGroupAuditEntries(Integer groupId, Integer offset, Integer limit) throws ClientException {
+ try {
+ // Validate group first
+ LookupGroup lookupGroup = LookupService.getInstance().getGroupById(groupId);
+ if (lookupGroup == null) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ }
+
+ // Normalize pagination and pattern
+ int safeOffset = offset != null ? offset : 0;
+ int safeLimit = limit != null ? limit : Integer.MAX_VALUE;
+
+ List entries = LookupService.getInstance().getAuditEntries(groupId, safeOffset, safeLimit);
+ int totalCount = LookupService.getInstance().getAuditEntryCount(groupId);
+
+ List users = userController.getAllUsers();
+
+ GroupAuditEntriesResponse response = GroupAuditEntriesResponse.fromResult(groupId, entries, totalCount, safeLimit, safeOffset, users);
+
+ return JsonUtils.toJson(response);
+
+ } catch (LookupApiException e) {
+ throw e;
+ } catch (Exception e) {
+ // Catch-all for unexpected internal errors
+ throw new ClientException("Failed to process getGroupAuditEntries request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String searchGroupAuditEntries(Integer groupId, Integer offset, Integer limit, String filterState) throws ClientException {
+ try {
+ HistoryFilterState filter = (filterState != null && !filterState.isEmpty())
+ ? HistoryFilterState.fromJson(filterState)
+ : new HistoryFilterState();
+
+ // Validate group first
+ LookupGroup lookupGroup = LookupService.getInstance().getGroupById(groupId);
+ if (lookupGroup == null) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ }
+
+ // Normalize pagination and pattern
+ int safeOffset = offset != null ? offset : 0;
+ int safeLimit = limit != null ? limit : 100;
+
+ List entries = LookupService.getInstance().searchAuditEntries(groupId, safeOffset, safeLimit, filter);
+ int totalCount = LookupService.getInstance().searchAuditEntryCount(groupId, filter);
+
+ List users = userController.getAllUsers();
+
+ GroupAuditEntriesResponse response = GroupAuditEntriesResponse.fromResult(groupId, entries, totalCount, safeLimit, safeOffset, users);
+
+ return JsonUtils.toJson(response);
+ } catch (LookupApiException e) {
+ throw e;
+ } catch (Exception e) {
+ // Catch-all for unexpected internal errors
+ throw new ClientException("Failed to process searchGroupAuditEntries request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String clearGroupCache(Integer groupId) throws ClientException {
+ try {
+ LookupService.getInstance().clearGroupCache(groupId);
+
+ Map response = new HashMap<>();
+ response.put("status", "success");
+ response.put("message", "Cache cleared successfully for group ID: " + groupId);
+
+ return JsonUtils.toJson(response);
+
+ } catch (GroupNotFoundException e) {
+ throw new LookupApiException(Response.Status.NOT_FOUND, LookupErrorCode.GROUP_NOT_FOUND, "Lookup group not found with ID: " + groupId);
+ } catch (Exception e) {
+ // Catch-all for unexpected internal errors
+ throw new ClientException("Failed to process getGroupStatistics request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+
+ @Override
+ public String clearAllCaches() throws ClientException {
+ try {
+ LookupService.getInstance().clearAllCaches();
+
+ Map response = new HashMap<>();
+ response.put("status", "success");
+ response.put("message", "All caches cleared successfully");
+
+ return JsonUtils.toJson(response);
+
+ } catch (Exception e) {
+ throw new ClientException("Failed to process clearAllCaches request. Error: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()), e);
+ }
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/userutil/LookupHelper.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/userutil/LookupHelper.java
new file mode 100644
index 000000000..c72d94c39
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/userutil/LookupHelper.java
@@ -0,0 +1,250 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.userutil;
+
+import com.mirth.connect.plugins.dynamiclookup.server.service.LookupService;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.CacheStatistics;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupStatistics;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides LookupHelper utility methods.
+ */
+public class LookupHelper {
+ private static final String SYSTEM_USER_ID = "0";
+ private static final Logger logger = LogManager.getLogger(LookupHelper.class);
+ private static LookupService lookupService;
+
+ /**
+ * Initializes the helper with a reference to the lookup service
+ */
+ public static void initialize(LookupService service) {
+ lookupService = service;
+ }
+
+ /**
+ * Retrieves a value from a lookup group without applying TTL validation.
+ * Always returns the best available value from cache or database.
+ *
+ * @param groupName the name of the lookup group
+ * @param key the lookup key
+ * @return the value, or null if not found or error occurs
+ */
+ public static String get(String groupName, String key) {
+ try {
+ LookupGroup group = lookupService.getGroupByName(groupName);
+ if (group == null) {
+ logger.error("Lookup group not found: {}", groupName);
+ return null;
+ }
+
+ return lookupService.getValue(group.getId(), key);
+ } catch (Exception e) {
+ logger.error("Failed to retrieve lookup value [group='{}', key='{}']: {}", groupName, key, e.getMessage(), e);
+ return null;
+ }
+ }
+
+ /**
+ * Retrieves a value from a lookup group using TTL validation.
+ * Returns null if the cached or database value is older than the specified TTL.
+ *
+ * @param groupName the name of the lookup group
+ * @param key the lookup key
+ * @param ttlHours maximum age in hours for the value to be considered valid (0 = no TTL enforcement)
+ * @return the value if valid within TTL, otherwise null
+ */
+ public static String get(String groupName, String key, long ttlHours) {
+ try {
+ LookupGroup group = lookupService.getGroupByName(groupName);
+ if (group == null) {
+ logger.error("Lookup group not found: {}", groupName);
+ return null;
+ }
+
+ return lookupService.getValue(group.getId(), key, ttlHours);
+ } catch (Exception e) {
+ logger.error("Failed to retrieve lookup value [group='{}', key='{}', ttl={}]: {}", groupName, key, ttlHours, e.getMessage(), e);
+ return null;
+ }
+ }
+
+ /**
+ * Retrieves a value from a lookup group with a fallback default if the key is not found or null.
+ * TTL is not applied — any available value (cached or from DB) will be returned.
+ *
+ * @param groupName the name of the lookup group
+ * @param key the lookup key
+ * @param defaultValue the fallback value to return if the lookup fails
+ * @return the lookup value if found, otherwise the provided default
+ */
+ public static String get(String groupName, String key, String defaultValue) {
+ String value = get(groupName, key);
+ return value != null ? value : defaultValue;
+ }
+
+ /**
+ * Retrieves a value from a lookup group with TTL enforcement and a fallback default.
+ * If the cached or DB value is older than the TTL, or not found, the default is returned.
+ *
+ * @param groupName the name of the lookup group
+ * @param key the lookup key
+ * @param ttlHours the TTL in hours; 0 or less means TTL is ignored
+ * @param defaultValue the fallback value to return if lookup fails or is stale
+ * @return the value if found and valid within TTL, otherwise the provided default
+ */
+ public static String get(String groupName, String key, long ttlHours, String defaultValue) {
+ String value = get(groupName, key, ttlHours);
+ return value != null ? value : defaultValue;
+ }
+
+ /**
+ * Retrieves values matching a pattern
+ */
+ public static Map getMatching(String groupName, String keyPattern) {
+ try {
+ LookupGroup group = lookupService.getGroupByName(groupName);
+ if (group == null) {
+ logger.error("Lookup group not found: {}", groupName);
+ return Collections.emptyMap();
+ }
+
+ return lookupService.getMatchingValues(group.getId(), keyPattern);
+ } catch (Exception e) {
+ logger.error("Failed to retrieve matching lookup values [group='{}', pattern='{}']: {}", groupName, keyPattern, e.getMessage(), e);
+ return Collections.emptyMap();
+ }
+ }
+
+ /**
+ * Retrieves multiple values at once from a lookup group without applying TTL validation.
+ * This method returns all available values found in the cache or database,
+ * regardless of their age or update time.
+ *
+ * @param groupName the name of the lookup group
+ * @param keys the list of keys to retrieve
+ * @return a map of key-value pairs; keys not found will be excluded from the result
+ */
+ public static Map getBatch(String groupName, List keys) {
+ try {
+ LookupGroup group = lookupService.getGroupByName(groupName);
+ if (group == null) {
+ logger.error("Lookup group not found: {}", groupName);
+ return Collections.emptyMap();
+ }
+
+ return lookupService.getBatchValues(group.getId(), keys);
+ } catch (Exception e) {
+ logger.error("Error in lookup operation: {}", e.getMessage(), e);
+ return Collections.emptyMap();
+ }
+ }
+
+ /**
+ * Retrieves multiple values at once from a lookup group using TTL validation.
+ * For each key, if the cached or database value is older than the specified TTL, it is excluded from the result.
+ *
+ * @param groupName the name of the lookup group
+ * @param keys the list of keys to retrieve
+ * @param ttlHours TTL in hours (0 = no TTL applied)
+ * @return a map of valid key-value pairs; excludes keys with missing or stale data
+ */
+ public static Map getBatch(String groupName, List keys, long ttlHours) {
+ try {
+ LookupGroup group = lookupService.getGroupByName(groupName);
+ if (group == null) {
+ logger.error("Lookup group not found: {}", groupName);
+ return Collections.emptyMap();
+ }
+
+ return lookupService.getBatchValues(group.getId(), keys, ttlHours);
+ } catch (Exception e) {
+ logger.error("Error in TTL-based lookup operation: {}", e.getMessage(), e);
+ return Collections.emptyMap();
+ }
+ }
+
+ /**
+ * Checks if a key exists in a group
+ */
+ public static boolean exists(String groupName, String key) {
+ try {
+ return get(groupName, key) != null;
+ } catch (Exception e) {
+ logger.error("Error checking key existence: {}", e.getMessage(), e);
+ return false;
+ }
+ }
+
+ /**
+ * Gets cache statistics for a group
+ */
+ public static Map getCacheStats(String groupName) {
+ try {
+ LookupGroup group = lookupService.getGroupByName(groupName);
+ if (group == null) {
+ logger.error("Lookup group not found: {}", groupName);
+ return Collections.emptyMap();
+ }
+
+ CacheStatistics cacheStatistics = lookupService.getCacheStatistics(group.getId());
+ LookupStatistics dbStats = lookupService.getStatistics(group.getId());
+
+ Map result = new HashMap<>();
+ if (cacheStatistics != null) {
+ result.put("hitCount", cacheStatistics.getHitCount());
+ result.put("missCount", cacheStatistics.getMissCount());
+ result.put("hitRatio", cacheStatistics.getHitRatio());
+ result.put("missRatio", cacheStatistics.getMissRatio());
+ result.put("evictionCount", cacheStatistics.getEvictionCount());
+ }
+
+ if (dbStats != null) {
+ result.put("totalLookups", dbStats.getTotalLookups());
+ result.put("cacheHits", dbStats.getCacheHits());
+ result.put("lastAccessed", dbStats.getLastAccessed());
+ }
+
+ return result;
+ } catch (Exception e) {
+ logger.error("Error getting cache stats: {}", e.getMessage(), e);
+ return Collections.emptyMap();
+ }
+ }
+
+ /**
+ * Sets a lookup value in the specified group
+ */
+ public static boolean set(String groupName, String key, String value) {
+ try {
+ LookupGroup group = lookupService.getGroupByName(groupName);
+ if (group == null) {
+ logger.error("Lookup group not found: {}", groupName);
+ return false;
+ }
+
+ lookupService.setValue(group.getId(), key, value, SYSTEM_USER_ID);
+
+ return true;
+ } catch (Exception e) {
+ logger.error("Failed to set lookup value [group='{}', key='{}']: {}", groupName, key, e.getMessage(), e);
+ return false;
+ }
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/util/SqlSessionManagerProvider.java b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/util/SqlSessionManagerProvider.java
new file mode 100644
index 000000000..4ec04dce8
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/java/com/mirth/connect/plugins/dynamiclookup/server/util/SqlSessionManagerProvider.java
@@ -0,0 +1,86 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.server.util;
+
+import com.mirth.connect.donkey.model.DatabaseConstants;
+import com.mirth.connect.model.converters.DocumentSerializer;
+import com.mirth.connect.plugins.dynamiclookup.server.config.DatabaseSettings;
+import com.mirth.connect.plugins.dynamiclookup.server.config.DatabaseSettingsLoader;
+import com.mirth.connect.server.util.SqlConfig;
+
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionManager;
+import org.apache.ibatis.io.Resources;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import java.io.BufferedReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.Properties;
+
+public class SqlSessionManagerProvider {
+
+ private static final Logger logger = LogManager.getLogger(SqlSessionManagerProvider.class);
+
+ private static SqlSessionManager sessionManager;
+
+ public static synchronized SqlSessionManager get() {
+ if (sessionManager != null) {
+ return sessionManager;
+ }
+
+ DatabaseSettings settings = DatabaseSettingsLoader.load();
+
+ if (!settings.isUseExternalDb()) {
+ sessionManager = SqlConfig.getInstance().getSqlSessionManager();
+ return sessionManager;
+ }
+
+ try {
+ // Reuse Mirth’s logic but build our own
+ SqlSessionFactory factory = createFactory(settings.getDatabase(), settings);
+ sessionManager = SqlSessionManager.newInstance(factory);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create external SqlSessionManager", e);
+ }
+
+ return sessionManager;
+ }
+
+ private static SqlSessionFactory createFactory(String database, DatabaseSettings settings) throws Exception {
+ // Load and parse sqlmap-config.xml
+ BufferedReader reader = new BufferedReader(Resources.getResourceAsReader("config/sqlmap-config.xml"));
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false);
+ Document document = factory.newDocumentBuilder().parse(new InputSource(reader));
+ factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+
+ // Serialize updated XML document back to reader
+ DocumentSerializer serializer = new DocumentSerializer();
+ Reader finalReader = new StringReader(serializer.toXML(document));
+
+ // Build properties from database settings
+ Properties props = settings.getProperties();
+ props.setProperty(DatabaseConstants.DATABASE, database);
+
+ return new SqlSessionFactoryBuilder().build(finalReader, "external", props);
+ }
+
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/config/sqlmap-config.xml b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/config/sqlmap-config.xml
new file mode 100644
index 000000000..287d8e13e
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/config/sqlmap-config.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/derby-lookup.xml b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/derby-lookup.xml
new file mode 100644
index 000000000..f3cd2c3f8
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/derby-lookup.xml
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LOWER(KEY_VALUE) LIKE LOWER('%' || #{pattern} || '%')
+ OR LOWER(VALUE_DATA) LIKE LOWER('%' || #{pattern} || '%')
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_GROUP (NAME, DESCRIPTION, VERSION, CACHE_SIZE, CACHE_POLICY, CREATED_DATE, UPDATED_DATE)
+ VALUES (#{name}, #{description}, #{version}, #{cacheSize}, #{cachePolicy}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+
+
+
+ UPDATE LOOKUP_GROUP
+ SET NAME = #{name},
+ DESCRIPTION = #{description},
+ VERSION = #{version},
+ CACHE_SIZE = #{cacheSize},
+ CACHE_POLICY = #{cachePolicy},
+ UPDATED_DATE = CURRENT_TIMESTAMP
+ WHERE ID = #{id}
+
+
+
+ DELETE FROM LOOKUP_GROUP
+ WHERE ID = #{value}
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO ${tableName} (KEY_VALUE, VALUE_DATA, CREATED_DATE, UPDATED_DATE)
+ VALUES (#{keyValue}, #{valueData}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+
+
+
+ UPDATE ${tableName}
+ SET VALUE_DATA = #{valueData},
+ UPDATED_DATE = CURRENT_TIMESTAMP
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+
+
+
+
+
+ CREATE TABLE ${tableName} (
+ KEY_VALUE VARCHAR(255) NOT NULL,
+ VALUE_DATA CLOB,
+ CREATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UPDATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (KEY_VALUE)
+ )
+
+
+
+ DROP TABLE ${tableName}
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_STATISTICS (
+ GROUP_ID, TOTAL_LOOKUPS, CACHE_HITS, LAST_ACCESSED, RESET_DATE ) VALUES ( #{groupId}, 0, 0, NULL, NULL )
+
+
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET
+ TOTAL_LOOKUPS = TOTAL_LOOKUPS + 1,
+ CACHE_HITS = CACHE_HITS +
+
+ 1
+ 0
+
+ ,
+ LAST_ACCESSED = #{lastAccessed}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET
+ TOTAL_LOOKUPS = 0,
+ CACHE_HITS = 0,
+ LAST_ACCESSED = NULL,
+ RESET_DATE = #{resetDate}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+
+ INSERT INTO LOOKUP_AUDIT (
+ GROUP_ID, TABLE_NAME, KEY_VALUE, ACTION, OLD_VALUE, NEW_VALUE, USER_ID, AUDIT_TIMESTAMP
+ ) VALUES (
+ #{groupId}, #{tableName}, #{keyValue}, #{action}, #{oldValue}, #{newValue}, #{userId}, CURRENT_TIMESTAMP
+ )
+
+
+
+
+
+
+
+
+ GROUP_ID = #{groupId}
+
+ AND LOWER(KEY_VALUE) LIKE LOWER('%' || #{keyValue} || '%')
+
+
+ AND ACTION = #{action}
+
+
+ AND USER_ID = #{userId}
+
+
+ AND AUDIT_TIMESTAMP >= #{startDate}
+
+
+ AND AUDIT_TIMESTAMP <= #{endDate}
+
+
+
+
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/lookup.xml b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/lookup.xml
new file mode 100644
index 000000000..f3cd2c3f8
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/lookup.xml
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LOWER(KEY_VALUE) LIKE LOWER('%' || #{pattern} || '%')
+ OR LOWER(VALUE_DATA) LIKE LOWER('%' || #{pattern} || '%')
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_GROUP (NAME, DESCRIPTION, VERSION, CACHE_SIZE, CACHE_POLICY, CREATED_DATE, UPDATED_DATE)
+ VALUES (#{name}, #{description}, #{version}, #{cacheSize}, #{cachePolicy}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+
+
+
+ UPDATE LOOKUP_GROUP
+ SET NAME = #{name},
+ DESCRIPTION = #{description},
+ VERSION = #{version},
+ CACHE_SIZE = #{cacheSize},
+ CACHE_POLICY = #{cachePolicy},
+ UPDATED_DATE = CURRENT_TIMESTAMP
+ WHERE ID = #{id}
+
+
+
+ DELETE FROM LOOKUP_GROUP
+ WHERE ID = #{value}
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO ${tableName} (KEY_VALUE, VALUE_DATA, CREATED_DATE, UPDATED_DATE)
+ VALUES (#{keyValue}, #{valueData}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+
+
+
+ UPDATE ${tableName}
+ SET VALUE_DATA = #{valueData},
+ UPDATED_DATE = CURRENT_TIMESTAMP
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+
+
+
+
+
+ CREATE TABLE ${tableName} (
+ KEY_VALUE VARCHAR(255) NOT NULL,
+ VALUE_DATA CLOB,
+ CREATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UPDATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (KEY_VALUE)
+ )
+
+
+
+ DROP TABLE ${tableName}
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_STATISTICS (
+ GROUP_ID, TOTAL_LOOKUPS, CACHE_HITS, LAST_ACCESSED, RESET_DATE ) VALUES ( #{groupId}, 0, 0, NULL, NULL )
+
+
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET
+ TOTAL_LOOKUPS = TOTAL_LOOKUPS + 1,
+ CACHE_HITS = CACHE_HITS +
+
+ 1
+ 0
+
+ ,
+ LAST_ACCESSED = #{lastAccessed}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET
+ TOTAL_LOOKUPS = 0,
+ CACHE_HITS = 0,
+ LAST_ACCESSED = NULL,
+ RESET_DATE = #{resetDate}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+
+ INSERT INTO LOOKUP_AUDIT (
+ GROUP_ID, TABLE_NAME, KEY_VALUE, ACTION, OLD_VALUE, NEW_VALUE, USER_ID, AUDIT_TIMESTAMP
+ ) VALUES (
+ #{groupId}, #{tableName}, #{keyValue}, #{action}, #{oldValue}, #{newValue}, #{userId}, CURRENT_TIMESTAMP
+ )
+
+
+
+
+
+
+
+
+ GROUP_ID = #{groupId}
+
+ AND LOWER(KEY_VALUE) LIKE LOWER('%' || #{keyValue} || '%')
+
+
+ AND ACTION = #{action}
+
+
+ AND USER_ID = #{userId}
+
+
+ AND AUDIT_TIMESTAMP >= #{startDate}
+
+
+ AND AUDIT_TIMESTAMP <= #{endDate}
+
+
+
+
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/mysql-lookup.xml b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/mysql-lookup.xml
new file mode 100644
index 000000000..d1b3cac73
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/mysql-lookup.xml
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LOWER(KEY_VALUE) LIKE LOWER(CONCAT('%', #{pattern}, '%'))
+ OR LOWER(VALUE_DATA) LIKE LOWER(CONCAT('%', #{pattern}, '%'))
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_GROUP (NAME, DESCRIPTION, VERSION, CACHE_SIZE, CACHE_POLICY)
+ VALUES (#{name}, #{description}, #{version}, #{cacheSize}, #{cachePolicy})
+
+
+
+ UPDATE LOOKUP_GROUP
+ SET NAME = #{name},
+ DESCRIPTION = #{description},
+ VERSION = #{version},
+ CACHE_SIZE = #{cacheSize},
+ CACHE_POLICY = #{cachePolicy},
+ UPDATED_DATE = CURRENT_TIMESTAMP
+ WHERE ID = #{id}
+
+
+
+ DELETE FROM LOOKUP_GROUP
+ WHERE ID = #{value}
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO ${tableName} (KEY_VALUE, VALUE_DATA)
+ VALUES (#{keyValue}, #{valueData})
+
+
+
+ UPDATE ${tableName}
+ SET VALUE_DATA = #{valueData},
+ UPDATED_DATE = CURRENT_TIMESTAMP
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+
+
+
+
+
+ CREATE TABLE ${tableName} (
+ KEY_VALUE VARCHAR(255) NOT NULL,
+ VALUE_DATA TEXT,
+ CREATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UPDATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (KEY_VALUE)
+ )
+
+
+
+ DROP TABLE IF EXISTS ${tableName};
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_STATISTICS (
+ GROUP_ID, TOTAL_LOOKUPS, CACHE_HITS, LAST_ACCESSED, RESET_DATE ) VALUES ( #{groupId}, 0, 0, NULL, NULL )
+
+
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET
+ TOTAL_LOOKUPS = TOTAL_LOOKUPS + 1,
+ CACHE_HITS = CACHE_HITS +
+
+ 1
+ 0
+
+ ,
+ LAST_ACCESSED = #{lastAccessed}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET
+ TOTAL_LOOKUPS = 0,
+ CACHE_HITS = 0,
+ LAST_ACCESSED = NULL,
+ RESET_DATE = #{resetDate}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+
+ INSERT INTO LOOKUP_AUDIT (
+ GROUP_ID, TABLE_NAME, KEY_VALUE, ACTION, OLD_VALUE, NEW_VALUE, USER_ID, AUDIT_TIMESTAMP
+ ) VALUES (
+ #{groupId}, #{tableName}, #{keyValue}, #{action}, #{oldValue}, #{newValue}, #{userId}, CURRENT_TIMESTAMP
+ )
+
+
+
+
+
+
+
+
+ GROUP_ID = #{groupId}
+
+ AND LOWER(KEY_VALUE) LIKE LOWER(CONCAT('%', #{keyValue}, '%'))
+
+
+ AND ACTION = #{action}
+
+
+ AND USER_ID = #{userId}
+
+
+ AND AUDIT_TIMESTAMP = ]]> #{startDate}
+
+
+ AND AUDIT_TIMESTAMP #{endDate}
+
+
+
+
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/oracle-lookup.xml b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/oracle-lookup.xml
new file mode 100644
index 000000000..0afdb029e
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/oracle-lookup.xml
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LOWER(KEY_VALUE) LIKE LOWER('%' || #{pattern} || '%')
+ OR LOWER(VALUE_DATA) LIKE LOWER('%' || #{pattern} || '%')
+
+
+
+
+
+
+
+
+
+
+
+
+ #{offset}
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_GROUP (NAME, DESCRIPTION, VERSION, CACHE_SIZE, CACHE_POLICY)
+ VALUES (#{name}, #{description}, #{version}, #{cacheSize}, #{cachePolicy})
+
+
+
+ UPDATE LOOKUP_GROUP
+ SET NAME = #{name},
+ DESCRIPTION = #{description},
+ VERSION = #{version},
+ CACHE_SIZE = #{cacheSize},
+ CACHE_POLICY = #{cachePolicy},
+ UPDATED_DATE = SYSDATE
+ WHERE ID = #{id}
+
+
+
+ DELETE FROM LOOKUP_GROUP
+ WHERE ID = #{value}
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO ${tableName} (KEY_VALUE, VALUE_DATA)
+ VALUES (#{keyValue}, #{valueData})
+
+
+
+ UPDATE ${tableName}
+ SET VALUE_DATA = #{valueData},
+ UPDATED_DATE = SYSDATE
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+
+
+
+
+
+ CREATE TABLE ${tableName} (
+ KEY_VALUE VARCHAR2(255) NOT NULL,
+ VALUE_DATA CLOB,
+ CREATED_DATE TIMESTAMP DEFAULT SYSDATE,
+ UPDATED_DATE TIMESTAMP DEFAULT SYSDATE,
+ PRIMARY KEY (KEY_VALUE)
+ )
+
+
+
+ BEGIN
+ EXECUTE IMMEDIATE 'DROP TABLE ${tableName}';
+ EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE != -942 THEN
+ RAISE;
+ END IF;
+ END;
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_STATISTICS (
+ GROUP_ID, TOTAL_LOOKUPS, CACHE_HITS, LAST_ACCESSED, RESET_DATE
+ ) VALUES (
+ #{groupId}, 0, 0, NULL, NULL
+ )
+
+
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET TOTAL_LOOKUPS = TOTAL_LOOKUPS + 1,
+ CACHE_HITS = CACHE_HITS +
+
+ 1
+ 0
+
+ ,
+ LAST_ACCESSED = #{lastAccessed}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET TOTAL_LOOKUPS = 0,
+ CACHE_HITS = 0,
+ LAST_ACCESSED = NULL,
+ RESET_DATE = #{resetDate}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+
+ INSERT INTO LOOKUP_AUDIT (
+ GROUP_ID, TABLE_NAME, KEY_VALUE, ACTION, OLD_VALUE, NEW_VALUE, USER_ID, AUDIT_TIMESTAMP
+ ) VALUES (
+ #{groupId}, #{tableName}, #{keyValue}, #{action}, #{oldValue}, #{newValue}, #{userId}, SYSDATE
+ )
+
+
+
+
+
+
+
+
+ GROUP_ID = #{groupId}
+
+ AND LOWER(KEY_VALUE) LIKE LOWER('%' || #{keyValue} || '%')
+
+
+ AND ACTION = #{action}
+
+
+ AND USER_ID = #{userId}
+
+
+ AND AUDIT_TIMESTAMP = ]]> #{startDate}
+
+
+ AND AUDIT_TIMESTAMP #{endDate}
+
+
+
+
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/postgres-lookup.xml b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/postgres-lookup.xml
new file mode 100644
index 000000000..f9d3107ce
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/postgres-lookup.xml
@@ -0,0 +1,284 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KEY_VALUE ILIKE '%' || #{pattern} || '%'
+ OR VALUE_DATA ILIKE '%' || #{pattern} || '%'
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_GROUP (NAME, DESCRIPTION, VERSION, CACHE_SIZE, CACHE_POLICY)
+ VALUES (#{name}, #{description}, #{version}, #{cacheSize}, #{cachePolicy})
+
+
+
+ UPDATE LOOKUP_GROUP
+ SET NAME = #{name},
+ DESCRIPTION = #{description},
+ VERSION = #{version},
+ CACHE_SIZE = #{cacheSize},
+ CACHE_POLICY = #{cachePolicy},
+ UPDATED_DATE = CURRENT_TIMESTAMP
+ WHERE ID = #{id}
+
+
+
+ DELETE FROM LOOKUP_GROUP
+ WHERE ID = #{value}
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO ${tableName} (KEY_VALUE, VALUE_DATA)
+ VALUES (#{keyValue}, #{valueData})
+
+
+
+ UPDATE ${tableName}
+ SET VALUE_DATA = #{valueData},
+ UPDATED_DATE = CURRENT_TIMESTAMP
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+
+
+
+
+
+ CREATE TABLE ${tableName} (
+ KEY_VALUE VARCHAR(255) NOT NULL,
+ VALUE_DATA TEXT,
+ CREATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UPDATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (KEY_VALUE)
+ )
+
+
+
+ DROP TABLE IF EXISTS ${tableName};
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_STATISTICS (
+ GROUP_ID, TOTAL_LOOKUPS, CACHE_HITS, LAST_ACCESSED, RESET_DATE ) VALUES ( #{groupId}, 0, 0, NULL, NULL )
+
+
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET
+ TOTAL_LOOKUPS = TOTAL_LOOKUPS + 1,
+ CACHE_HITS = CACHE_HITS +
+
+ 1
+ 0
+
+ ,
+ LAST_ACCESSED = #{lastAccessed}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET
+ TOTAL_LOOKUPS = 0,
+ CACHE_HITS = 0,
+ LAST_ACCESSED = NULL,
+ RESET_DATE = #{resetDate}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+
+ INSERT INTO LOOKUP_AUDIT (
+ GROUP_ID, TABLE_NAME, KEY_VALUE, ACTION, OLD_VALUE, NEW_VALUE, USER_ID, AUDIT_TIMESTAMP
+ ) VALUES (
+ #{groupId}, #{tableName}, #{keyValue}, #{action}, #{oldValue}, #{newValue}, #{userId}, CURRENT_TIMESTAMP
+ )
+
+
+
+
+
+
+
+
+ GROUP_ID = #{groupId}
+
+ AND KEY_VALUE ILIKE '%' || #{keyValue} || '%'
+
+
+ AND ACTION = #{action}
+
+
+ AND USER_ID = #{userId}
+
+
+ AND AUDIT_TIMESTAMP = ]]> #{startDate}
+
+
+ AND AUDIT_TIMESTAMP #{endDate}
+
+
+
+
+
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/sqlserver-lookup.xml b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/sqlserver-lookup.xml
new file mode 100644
index 000000000..dba5e0e76
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/mapper/sqlserver-lookup.xml
@@ -0,0 +1,279 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (
+ LOWER(KEY_VALUE) LIKE '%' + LOWER(#{pattern}) + '%'
+ OR LOWER(VALUE_DATA) LIKE '%' + LOWER(#{pattern}) + '%'
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_GROUP (NAME, DESCRIPTION, VERSION, CACHE_SIZE, CACHE_POLICY)
+ VALUES (#{name}, #{description}, #{version}, #{cacheSize}, #{cachePolicy})
+
+
+
+ UPDATE LOOKUP_GROUP
+ SET NAME = #{name},
+ DESCRIPTION = #{description},
+ VERSION = #{version},
+ CACHE_SIZE = #{cacheSize},
+ CACHE_POLICY = #{cachePolicy},
+ UPDATED_DATE = CURRENT_TIMESTAMP
+ WHERE ID = #{id}
+
+
+
+ DELETE FROM LOOKUP_GROUP
+ WHERE ID = #{value}
+
+
+
+
+
+
+
+
+
+
+
+
+ INSERT INTO ${tableName} (KEY_VALUE, VALUE_DATA)
+ VALUES (#{keyValue}, #{valueData})
+
+
+
+ UPDATE ${tableName}
+ SET VALUE_DATA = #{valueData},
+ UPDATED_DATE = CURRENT_TIMESTAMP
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+ WHERE KEY_VALUE = #{keyValue}
+
+
+
+ DELETE FROM ${tableName}
+
+
+
+
+
+ CREATE TABLE ${tableName} (
+ KEY_VALUE VARCHAR(255) NOT NULL,
+ VALUE_DATA VARCHAR(MAX),
+ CREATED_DATE DATETIME DEFAULT GETDATE(),
+ UPDATED_DATE DATETIME DEFAULT GETDATE(),
+ PRIMARY KEY (KEY_VALUE)
+ )
+
+
+
+ IF OBJECT_ID('${tableName}', 'U') IS NOT NULL
+ DROP TABLE ${tableName};
+
+
+
+
+
+
+
+
+ INSERT INTO LOOKUP_STATISTICS (
+ GROUP_ID, TOTAL_LOOKUPS, CACHE_HITS, LAST_ACCESSED, RESET_DATE ) VALUES ( #{groupId}, 0, 0, NULL, NULL )
+
+
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET
+ TOTAL_LOOKUPS = TOTAL_LOOKUPS + 1,
+ CACHE_HITS = CACHE_HITS +
+
+ 1
+ 0
+
+ ,
+ LAST_ACCESSED = #{lastAccessed}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+ UPDATE LOOKUP_STATISTICS
+ SET
+ TOTAL_LOOKUPS = 0,
+ CACHE_HITS = 0,
+ LAST_ACCESSED = NULL,
+ RESET_DATE = #{resetDate}
+ WHERE GROUP_ID = #{groupId}
+
+
+
+
+ INSERT INTO LOOKUP_AUDIT (
+ GROUP_ID, TABLE_NAME, KEY_VALUE, ACTION, OLD_VALUE, NEW_VALUE, USER_ID, AUDIT_TIMESTAMP
+ ) VALUES (
+ #{groupId}, #{tableName}, #{keyValue}, #{action}, #{oldValue}, #{newValue}, #{userId}, CURRENT_TIMESTAMP
+ )
+
+
+
+
+
+
+
+
+ GROUP_ID = #{groupId}
+
+ AND ( LOWER(KEY_VALUE) LIKE '%' + LOWER(#{keyValue}) + '%' )
+
+
+ AND ACTION = #{action}
+
+
+ AND USER_ID = #{userId}
+
+
+ AND AUDIT_TIMESTAMP = ]]> #{startDate}
+
+
+ AND AUDIT_TIMESTAMP #{endDate}
+
+
+
+
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/derby/create_lookup_tables.sql b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/derby/create_lookup_tables.sql
new file mode 100644
index 000000000..58e4a9e8f
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/derby/create_lookup_tables.sql
@@ -0,0 +1,45 @@
+-- Derby Migration script for Lookup Table Management System
+
+-- Create LOOKUP_GROUP table
+CREATE TABLE LOOKUP_GROUP (
+ ID INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1),
+ NAME VARCHAR(255) NOT NULL,
+ DESCRIPTION CLOB,
+ VERSION VARCHAR(50),
+ CACHE_SIZE INTEGER DEFAULT 1000,
+ CACHE_POLICY VARCHAR(50) DEFAULT 'LRU',
+ CREATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UPDATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT PK_LOOKUP_GROUP PRIMARY KEY (ID),
+ CONSTRAINT UQ_LOOKUP_GROUP_NAME UNIQUE (NAME)
+);
+
+-- Create LOOKUP_AUDIT table
+CREATE TABLE LOOKUP_AUDIT (
+ ID BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1),
+ GROUP_ID INTEGER NOT NULL,
+ TABLE_NAME VARCHAR(255) NOT NULL,
+ KEY_VALUE VARCHAR(255) NOT NULL,
+ ACTION VARCHAR(50) NOT NULL,
+ OLD_VALUE CLOB,
+ NEW_VALUE CLOB,
+ USER_ID VARCHAR(255),
+ AUDIT_TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT PK_LOOKUP_AUDIT PRIMARY KEY (ID),
+ CONSTRAINT FK_LOOKUP_AUDIT_GROUP FOREIGN KEY (GROUP_ID) REFERENCES LOOKUP_GROUP(ID) ON DELETE CASCADE
+);
+
+-- Create indexes for LOOKUP_AUDIT
+CREATE INDEX IDX_LOOKUP_AUDIT_GROUP ON LOOKUP_AUDIT (GROUP_ID);
+CREATE INDEX IDX_LOOKUP_AUDIT_KEY ON LOOKUP_AUDIT (TABLE_NAME, KEY_VALUE);
+
+-- Create LOOKUP_STATISTICS table
+CREATE TABLE LOOKUP_STATISTICS (
+ GROUP_ID INTEGER NOT NULL,
+ TOTAL_LOOKUPS BIGINT DEFAULT 0,
+ CACHE_HITS BIGINT DEFAULT 0,
+ LAST_ACCESSED TIMESTAMP,
+ RESET_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT PK_LOOKUP_STATISTICS PRIMARY KEY (GROUP_ID),
+ CONSTRAINT FK_LOOKUP_STATISTICS_GROUP FOREIGN KEY (GROUP_ID) REFERENCES LOOKUP_GROUP(ID) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/mysql/create_lookup_tables.sql b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/mysql/create_lookup_tables.sql
new file mode 100644
index 000000000..d73c5d5c2
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/mysql/create_lookup_tables.sql
@@ -0,0 +1,45 @@
+-- MySQL Migration script for Lookup Table Management System
+
+-- Create LOOKUP_GROUP table
+CREATE TABLE LOOKUP_GROUP (
+ ID INTEGER NOT NULL AUTO_INCREMENT,
+ NAME VARCHAR(255) NOT NULL,
+ DESCRIPTION TEXT,
+ VERSION VARCHAR(50),
+ CACHE_SIZE INTEGER DEFAULT 1000,
+ CACHE_POLICY VARCHAR(50) DEFAULT 'LRU',
+ CREATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UPDATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (ID)
+) ENGINE=InnoDB;
+
+CREATE UNIQUE INDEX IDX_LOOKUP_GROUP_NAME ON LOOKUP_GROUP (NAME);
+
+-- Create LOOKUP_AUDIT table
+CREATE TABLE LOOKUP_AUDIT (
+ ID BIGINT NOT NULL AUTO_INCREMENT,
+ GROUP_ID INTEGER NOT NULL,
+ TABLE_NAME VARCHAR(255) NOT NULL,
+ KEY_VALUE VARCHAR(255) NOT NULL,
+ ACTION VARCHAR(50) NOT NULL,
+ OLD_VALUE TEXT,
+ NEW_VALUE TEXT,
+ USER_ID VARCHAR(255),
+ AUDIT_TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (ID),
+ FOREIGN KEY (GROUP_ID) REFERENCES LOOKUP_GROUP(ID) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE INDEX IDX_LOOKUP_AUDIT_GROUP ON LOOKUP_AUDIT (GROUP_ID);
+CREATE INDEX IDX_LOOKUP_AUDIT_KEY ON LOOKUP_AUDIT (TABLE_NAME, KEY_VALUE);
+
+-- Optional statistics table
+CREATE TABLE LOOKUP_STATISTICS (
+ GROUP_ID INTEGER NOT NULL,
+ TOTAL_LOOKUPS BIGINT DEFAULT 0,
+ CACHE_HITS BIGINT DEFAULT 0,
+ LAST_ACCESSED TIMESTAMP NULL,
+ RESET_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (GROUP_ID),
+ FOREIGN KEY (GROUP_ID) REFERENCES LOOKUP_GROUP(ID) ON DELETE CASCADE
+) ENGINE=InnoDB;
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/oracle/create_lookup_tables.sql b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/oracle/create_lookup_tables.sql
new file mode 100644
index 000000000..c802c6d97
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/oracle/create_lookup_tables.sql
@@ -0,0 +1,46 @@
+-- Oracle Migration script for Lookup Table Management System
+
+-- Create LOOKUP_GROUP table
+CREATE TABLE LOOKUP_GROUP (
+ ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ NAME VARCHAR2(255) NOT NULL,
+ DESCRIPTION CLOB,
+ VERSION VARCHAR2(50),
+ CACHE_SIZE NUMBER DEFAULT 1000,
+ CACHE_POLICY VARCHAR2(50) DEFAULT 'LRU',
+ CREATED_DATE TIMESTAMP DEFAULT SYSDATE,
+ UPDATED_DATE TIMESTAMP DEFAULT SYSDATE
+);
+
+
+CREATE UNIQUE INDEX IDX_LOOKUP_GROUP_NAME ON LOOKUP_GROUP (NAME);
+
+-- Create LOOKUP_AUDIT table
+CREATE TABLE LOOKUP_AUDIT (
+ ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ GROUP_ID NUMBER NOT NULL,
+ TABLE_NAME VARCHAR2(255) NOT NULL,
+ KEY_VALUE VARCHAR2(255) NOT NULL,
+ ACTION VARCHAR2(50) NOT NULL,
+ OLD_VALUE CLOB,
+ NEW_VALUE CLOB,
+ USER_ID VARCHAR2(255),
+ AUDIT_TIMESTAMP TIMESTAMP DEFAULT SYSDATE,
+ CONSTRAINT FK_LOOKUP_AUDIT_GROUP FOREIGN KEY (GROUP_ID)
+ REFERENCES LOOKUP_GROUP(ID) ON DELETE CASCADE
+);
+
+-- Add indexes
+CREATE INDEX IDX_LOOKUP_AUDIT_GROUP ON LOOKUP_AUDIT (GROUP_ID);
+CREATE INDEX IDX_LOOKUP_AUDIT_KEY ON LOOKUP_AUDIT (TABLE_NAME, KEY_VALUE);
+
+-- Optional statistics table
+CREATE TABLE LOOKUP_STATISTICS (
+ GROUP_ID NUMBER PRIMARY KEY,
+ TOTAL_LOOKUPS NUMBER DEFAULT 0,
+ CACHE_HITS NUMBER DEFAULT 0,
+ LAST_ACCESSED TIMESTAMP NULL,
+ RESET_DATE TIMESTAMP DEFAULT SYSDATE,
+ CONSTRAINT FK_LOOKUP_STATS_GROUP FOREIGN KEY (GROUP_ID)
+ REFERENCES LOOKUP_GROUP(ID) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/postgres/create_lookup_tables.sql b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/postgres/create_lookup_tables.sql
new file mode 100644
index 000000000..c0720efb9
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/postgres/create_lookup_tables.sql
@@ -0,0 +1,43 @@
+-- PostgreSQL Migration script for Lookup Table Management System
+
+-- Create LOOKUP_GROUP table
+CREATE TABLE LOOKUP_GROUP (
+ ID SERIAL PRIMARY KEY,
+ NAME VARCHAR(255) NOT NULL,
+ DESCRIPTION TEXT,
+ VERSION VARCHAR(50),
+ CACHE_SIZE INTEGER DEFAULT 1000,
+ CACHE_POLICY VARCHAR(50) DEFAULT 'LRU',
+ CREATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UPDATED_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE UNIQUE INDEX IDX_LOOKUP_GROUP_NAME ON LOOKUP_GROUP (NAME);
+
+-- Create LOOKUP_AUDIT table
+CREATE TABLE LOOKUP_AUDIT (
+ ID BIGSERIAL PRIMARY KEY,
+ GROUP_ID INTEGER NOT NULL,
+ TABLE_NAME VARCHAR(255) NOT NULL,
+ KEY_VALUE VARCHAR(255) NOT NULL,
+ ACTION VARCHAR(50) NOT NULL,
+ OLD_VALUE TEXT,
+ NEW_VALUE TEXT,
+ USER_ID VARCHAR(255),
+ AUDIT_TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (GROUP_ID) REFERENCES LOOKUP_GROUP(ID) ON DELETE CASCADE
+);
+
+CREATE INDEX IDX_LOOKUP_AUDIT_GROUP ON LOOKUP_AUDIT (GROUP_ID);
+CREATE INDEX IDX_LOOKUP_AUDIT_KEY ON LOOKUP_AUDIT (TABLE_NAME, KEY_VALUE);
+
+-- Optional statistics table
+CREATE TABLE LOOKUP_STATISTICS (
+ GROUP_ID INTEGER NOT NULL,
+ TOTAL_LOOKUPS BIGINT DEFAULT 0,
+ CACHE_HITS BIGINT DEFAULT 0,
+ LAST_ACCESSED TIMESTAMP,
+ RESET_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (GROUP_ID),
+ FOREIGN KEY (GROUP_ID) REFERENCES LOOKUP_GROUP(ID) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/sqlserver/create_lookup_tables.sql b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/sqlserver/create_lookup_tables.sql
new file mode 100644
index 000000000..9b7e340c1
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/server/src/main/resources/sql/sqlserver/create_lookup_tables.sql
@@ -0,0 +1,42 @@
+-- SQL Server Migration script for Lookup Table Management System
+
+-- Create LOOKUP_GROUP table
+CREATE TABLE LOOKUP_GROUP (
+ ID INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
+ NAME VARCHAR(255) NOT NULL,
+ DESCRIPTION VARCHAR(MAX),
+ VERSION VARCHAR(50),
+ CACHE_SIZE INT DEFAULT 1000,
+ CACHE_POLICY VARCHAR(50) DEFAULT 'LRU',
+ CREATED_DATE DATETIME DEFAULT GETDATE(),
+ UPDATED_DATE DATETIME DEFAULT GETDATE()
+);
+
+CREATE UNIQUE INDEX IDX_LOOKUP_GROUP_NAME ON LOOKUP_GROUP (NAME);
+
+-- Create LOOKUP_AUDIT table
+CREATE TABLE LOOKUP_AUDIT (
+ ID BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY,
+ GROUP_ID INT NOT NULL,
+ TABLE_NAME VARCHAR(255) NOT NULL,
+ KEY_VALUE VARCHAR(255) NOT NULL,
+ ACTION VARCHAR(50) NOT NULL,
+ OLD_VALUE VARCHAR(MAX),
+ NEW_VALUE VARCHAR(MAX),
+ USER_ID VARCHAR(255),
+ AUDIT_TIMESTAMP DATETIME DEFAULT GETDATE(),
+ FOREIGN KEY (GROUP_ID) REFERENCES LOOKUP_GROUP(ID) ON DELETE CASCADE
+);
+
+CREATE INDEX IDX_LOOKUP_AUDIT_GROUP ON LOOKUP_AUDIT (GROUP_ID);
+CREATE INDEX IDX_LOOKUP_AUDIT_KEY ON LOOKUP_AUDIT (TABLE_NAME, KEY_VALUE);
+
+-- Optional statistics table
+CREATE TABLE LOOKUP_STATISTICS (
+ GROUP_ID INT NOT NULL PRIMARY KEY,
+ TOTAL_LOOKUPS BIGINT DEFAULT 0,
+ CACHE_HITS BIGINT DEFAULT 0,
+ LAST_ACCESSED DATETIME NULL,
+ RESET_DATE DATETIME DEFAULT GETDATE(),
+ FOREIGN KEY (GROUP_ID) REFERENCES LOOKUP_GROUP(ID) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/LookupModelMapper.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/LookupModelMapper.java
new file mode 100644
index 000000000..30bfd935b
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/LookupModelMapper.java
@@ -0,0 +1,74 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.request.BatchGetValuesRequest;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.request.ImportValuesRequest;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.request.LookupGroupRequest;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.request.LookupValueRequest;
+import com.mirth.connect.plugins.dynamiclookup.shared.dto.response.LookupValueResponse;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+
+import java.util.Date;
+import java.util.Map;
+import java.util.Collections;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+
+public class LookupModelMapper {
+ // --- Group Mapping ---
+ public static LookupGroup fromGroupDto(LookupGroupRequest dto) {
+ LookupGroup group = new LookupGroup();
+ group.setName(dto.getName());
+ group.setDescription(dto.getDescription());
+ group.setVersion(dto.getVersion());
+ group.setCacheSize(dto.getCacheSize());
+ group.setCachePolicy(dto.getCachePolicy());
+ group.setCreatedDate(new Date());
+ group.setUpdatedDate(new Date());
+ return group;
+ }
+
+ public static LookupValue fromValueDto(LookupValueRequest dto) {
+ LookupValue value = new LookupValue();
+
+ value.setValueData(dto.getValue());
+ value.setCreatedDate(new Date());
+ value.setCreatedDate(new Date());
+
+ return value;
+ }
+
+ public static Map fromImportValuesDto(ImportValuesRequest dto) {
+ if (dto == null || dto.getValues() == null) {
+ return Collections.emptyMap(); // return empty map instead of null
+ }
+ return new LinkedHashMap<>(dto.getValues()); // preserve insertion order
+ }
+
+ public static List fromBatchGetValues(BatchGetValuesRequest dto) {
+ if (dto == null || dto.getKeys() == null) {
+ return Collections.emptyList();
+ }
+ return new ArrayList<>(dto.getKeys());
+ }
+
+ // --- Value Mapping (entity -> response) ---
+ public static LookupValueResponse toValueResponse(LookupValue value, Integer groupId) {
+ return LookupValueResponse.from(value, groupId, true);
+ }
+
+ public static LookupValueResponse toValueResponse(LookupValue value, Integer groupId, boolean full) {
+ return LookupValueResponse.from(value, groupId, full);
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/BatchGetValuesRequest.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/BatchGetValuesRequest.java
new file mode 100644
index 000000000..7167d7bea
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/BatchGetValuesRequest.java
@@ -0,0 +1,39 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.request;
+
+import java.util.List;
+
+public class BatchGetValuesRequest {
+ private List keys;
+
+ public BatchGetValuesRequest() {
+ }
+
+ public BatchGetValuesRequest(List keys) {
+ this.keys = keys;
+ }
+
+ public List getKeys() {
+ return keys;
+ }
+
+ public void setKeys(List keys) {
+ this.keys = keys;
+ }
+
+ public void validate() {
+ if (keys == null || keys.isEmpty()) {
+ throw new IllegalArgumentException("The 'keys' list must not be null or empty.");
+ }
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/ImportLookupGroupRequest.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/ImportLookupGroupRequest.java
new file mode 100644
index 000000000..c7b181bd4
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/ImportLookupGroupRequest.java
@@ -0,0 +1,67 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.request;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+
+import java.util.Map;
+
+public class ImportLookupGroupRequest {
+ private LookupGroup group;
+ private Map values;
+
+ public LookupGroup getGroup() {
+ return group;
+ }
+
+ public void setGroup(LookupGroup group) {
+ this.group = group;
+ }
+
+ public Map getValues() {
+ return values;
+ }
+
+ public void setValues(Map values) {
+ this.values = values;
+ }
+
+ public void validate() {
+ if (group == null) {
+ throw new IllegalArgumentException("Group metadata is required.");
+ }
+
+ if (group.getName() == null || group.getName().trim().isEmpty()) {
+ throw new IllegalArgumentException("Group name is required.");
+ }
+
+ if (group.getVersion() == null || group.getVersion().trim().isEmpty()) {
+ throw new IllegalArgumentException("Group version is required.");
+ }
+
+ if (group.getCachePolicy() == null || group.getCachePolicy().trim().isEmpty()) {
+ throw new IllegalArgumentException("Cache policy is required.");
+ }
+
+ String policy = group.getCachePolicy().toUpperCase();
+ if (!policy.equals("LRU") && !policy.equals("FIFO")) {
+ throw new IllegalArgumentException("Cache policy must be either 'LRU' or 'FIFO'.");
+ }
+
+ if (group.getCacheSize() <= 0) {
+ throw new IllegalArgumentException("Cache size must be greater than zero.");
+ }
+
+ if (values != null && values.size() > 100_000) {
+ throw new IllegalArgumentException("Too many values in import request. Limit is 100,000.");
+ }
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/ImportValuesRequest.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/ImportValuesRequest.java
new file mode 100644
index 000000000..296f7c1eb
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/ImportValuesRequest.java
@@ -0,0 +1,48 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.request;
+
+import java.util.Map;
+
+public class ImportValuesRequest {
+ private Map values;
+
+ public ImportValuesRequest() {
+ }
+
+ public ImportValuesRequest(Map values) {
+ this.values = values;
+ }
+
+ public Map getValues() {
+ return values;
+ }
+
+ public void setValues(Map values) {
+ this.values = values;
+ }
+
+ public void validate() {
+ if (values == null || values.isEmpty()) {
+ throw new IllegalArgumentException("The 'values' object must not be null or empty.");
+ }
+
+ for (Map.Entry entry : values.entrySet()) {
+ if (entry.getKey() == null || entry.getKey().trim().isEmpty()) {
+ throw new IllegalArgumentException("Key cannot be null or empty.");
+ }
+ if (entry.getValue() == null || entry.getValue().trim().isEmpty()) {
+ throw new IllegalArgumentException("Value for key '" + entry.getKey() + "' cannot be null or empty.");
+ }
+ // Optional: add length, pattern, or format checks here
+ }
+ }
+}
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/LookupGroupRequest.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/LookupGroupRequest.java
new file mode 100644
index 000000000..05c5ad22b
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/LookupGroupRequest.java
@@ -0,0 +1,82 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.request;
+
+import org.apache.commons.lang3.StringUtils;
+
+public class LookupGroupRequest {
+ private String name; // Required
+ private String description; // Optional
+ private String version; // Required
+ private Integer cacheSize; // Required
+ private String cachePolicy; // Required
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public Integer getCacheSize() {
+ return cacheSize;
+ }
+
+ public void setCacheSize(Integer cacheSize) {
+ this.cacheSize = cacheSize;
+ }
+
+ public String getCachePolicy() {
+ return cachePolicy;
+ }
+
+ public void setCachePolicy(String cachePolicy) {
+ this.cachePolicy = cachePolicy;
+ }
+
+ public void validate() throws IllegalArgumentException {
+ if (name == null || StringUtils.isBlank(name)) {
+ throw new IllegalArgumentException("Missing required field: name");
+ }
+ if (version == null || StringUtils.isBlank(version)) {
+ throw new IllegalArgumentException("Missing required field: version");
+ }
+
+ if (cachePolicy == null || StringUtils.isBlank(cachePolicy)) {
+ throw new IllegalArgumentException("Missing required field: cachePolicy");
+ }
+
+ if (!cachePolicy.equalsIgnoreCase("LRU") && !cachePolicy.equalsIgnoreCase("FIFO")) {
+ throw new IllegalArgumentException("Invalid cachePolicy. Allowed values are: LRU, FIFO.");
+ }
+
+ if (cacheSize == null || cacheSize < 0) {
+ throw new IllegalArgumentException("Missing or invalid field: cacheSize");
+ }
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/LookupValueRequest.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/LookupValueRequest.java
new file mode 100644
index 000000000..3d422b485
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/request/LookupValueRequest.java
@@ -0,0 +1,31 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.request;
+
+import org.apache.commons.lang3.StringUtils;
+
+public class LookupValueRequest {
+ private String value;
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public void validate() throws IllegalArgumentException {
+ if (value == null || StringUtils.isBlank(value)) {
+ throw new IllegalArgumentException("Missing required field: value");
+ }
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/BatchGetValuesResponse.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/BatchGetValuesResponse.java
new file mode 100644
index 000000000..4469dd962
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/BatchGetValuesResponse.java
@@ -0,0 +1,54 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import java.util.List;
+import java.util.Map;
+
+public class BatchGetValuesResponse {
+ private Integer groupId;
+ private Map values;
+ private List missingKeys;
+
+ public BatchGetValuesResponse() {
+ }
+
+ public BatchGetValuesResponse(Integer groupId, Map values, List missingKeys) {
+ this.groupId = groupId;
+ this.values = values;
+ this.missingKeys = missingKeys;
+ }
+
+ public Integer getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(Integer groupId) {
+ this.groupId = groupId;
+ }
+
+ public Map getValues() {
+ return values;
+ }
+
+ public void setValues(Map values) {
+ this.values = values;
+ }
+
+ public List getMissingKeys() {
+ return missingKeys;
+ }
+
+ public void setMissingKeys(List missingKeys) {
+ this.missingKeys = missingKeys;
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/CacheStatistics.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/CacheStatistics.java
new file mode 100644
index 000000000..616f53cd8
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/CacheStatistics.java
@@ -0,0 +1,136 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A complete, serializable representation of in-memory cache performance
+ * for a lookup group, including raw stats, derived metrics, and configuration context.
+ */
+public class CacheStatistics {
+
+ // 1. Configuration & Support Context
+ private final boolean statsSupported;
+ private final String evictionPolicy;
+ private final int currentEntryCount;
+ private final int configuredMaxEntries;
+
+ // 2. Raw Stats (mirroring Guava's CacheStats)
+ private final long hitCount;
+ private final long missCount;
+ private final long loadSuccessCount;
+ private final long loadExceptionCount;
+ private final long totalLoadTime; // nanoseconds
+ private final long evictionCount;
+
+ // 3. Derived Metrics
+ private final double hitRatio;
+ private final double missRatio;
+ private final String totalLoadTimeFormatted;
+
+ @JsonCreator
+ public CacheStatistics(
+ @JsonProperty("statsSupported") boolean statsSupported,
+ @JsonProperty("evictionPolicy") String evictionPolicy,
+ @JsonProperty("currentEntryCount") int currentEntryCount,
+ @JsonProperty("configuredMaxEntries") int configuredMaxEntries,
+ @JsonProperty("hitCount") long hitCount,
+ @JsonProperty("missCount") long missCount,
+ @JsonProperty("loadSuccessCount") long loadSuccessCount,
+ @JsonProperty("loadExceptionCount") long loadExceptionCount,
+ @JsonProperty("totalLoadTime") long totalLoadTime,
+ @JsonProperty("evictionCount") long evictionCount
+ ) {
+
+ this.statsSupported = statsSupported;
+ this.evictionPolicy = evictionPolicy != null ? evictionPolicy.toUpperCase() : "UNKNOWN";
+ this.currentEntryCount = currentEntryCount;
+ this.configuredMaxEntries = configuredMaxEntries;
+
+ this.hitCount = hitCount;
+ this.missCount = missCount;
+ this.loadSuccessCount = loadSuccessCount;
+ this.loadExceptionCount = loadExceptionCount;
+ this.totalLoadTime = totalLoadTime;
+ this.evictionCount = evictionCount;
+
+ long totalRequests = hitCount + missCount;
+ this.hitRatio = (totalRequests > 0) ? (double) hitCount / totalRequests : 0.0;
+ this.missRatio = (totalRequests > 0) ? (double) missCount / totalRequests : 0.0;
+ this.totalLoadTimeFormatted = formatLoadTime(totalLoadTime);
+ }
+
+ private String formatLoadTime(long nanos) {
+ long millis = TimeUnit.NANOSECONDS.toMillis(nanos);
+ return (millis < 1000) ? millis + " ms" : (millis / 1000) + " sec";
+ }
+
+ // --- Getters ---
+
+ public boolean isStatsSupported() {
+ return statsSupported;
+ }
+
+ public String getEvictionPolicy() {
+ return evictionPolicy;
+ }
+
+ public int getCurrentEntryCount() {
+ return currentEntryCount;
+ }
+
+ public int getConfiguredMaxEntries() {
+ return configuredMaxEntries;
+ }
+
+ public long getHitCount() {
+ return hitCount;
+ }
+
+ public long getMissCount() {
+ return missCount;
+ }
+
+ public long getLoadSuccessCount() {
+ return loadSuccessCount;
+ }
+
+ public long getLoadExceptionCount() {
+ return loadExceptionCount;
+ }
+
+ public long getTotalLoadTime() {
+ return totalLoadTime;
+ }
+
+ public long getEvictionCount() {
+ return evictionCount;
+ }
+
+ public double getHitRatio() {
+ return hitRatio;
+ }
+
+ public double getMissRatio() {
+ return missRatio;
+ }
+
+ public String getTotalLoadTimeFormatted() {
+ return totalLoadTimeFormatted;
+ }
+}
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ErrorResponse.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ErrorResponse.java
new file mode 100644
index 000000000..c447fd828
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ErrorResponse.java
@@ -0,0 +1,61 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+public class ErrorResponse {
+ private String status;
+ private String code;
+ private String message;
+ private String timestamp;
+
+ public ErrorResponse() {
+ }
+
+ public ErrorResponse(String code, String message) {
+ this.status = "error";
+ this.code = code;
+ this.message = message;
+ this.timestamp = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC).toString();
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public String getTimestamp() {
+ return timestamp;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public void setCode(String code) {
+ this.code = code;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public void setTimestamp(String timestamp) {
+ this.timestamp = timestamp;
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ErrorResponseFactory.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ErrorResponseFactory.java
new file mode 100644
index 000000000..cf18d2ea4
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ErrorResponseFactory.java
@@ -0,0 +1,26 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import java.time.ZonedDateTime;
+import java.time.ZoneOffset;
+
+public class ErrorResponseFactory {
+
+ public static ErrorResponse build(String code, String message) {
+ ErrorResponse error = new ErrorResponse();
+ error.setStatus("error");
+ error.setCode(code);
+ error.setMessage(message);
+ error.setTimestamp(ZonedDateTime.now(ZoneOffset.UTC).toString());
+ return error;
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ExportGroupPagedResponse.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ExportGroupPagedResponse.java
new file mode 100644
index 000000000..a91b165ba
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ExportGroupPagedResponse.java
@@ -0,0 +1,134 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+
+import java.util.Map;
+import java.util.List;
+import java.util.LinkedHashMap;
+import java.util.Date;
+
+public class ExportGroupPagedResponse {
+
+ private Integer groupId;
+ private Integer totalCount;
+ private Pagination pagination;
+ private Map values;
+ private Date exportDate;
+
+ public ExportGroupPagedResponse() {
+ }
+
+ public ExportGroupPagedResponse(Integer groupId, Integer totalCount, Pagination pagination,
+ Map values, Date exportDate) {
+ this.groupId = groupId;
+ this.totalCount = totalCount;
+ this.pagination = pagination;
+ this.values = values;
+ this.exportDate = exportDate;
+ }
+
+ public static ExportGroupPagedResponse fromResult(
+ Integer groupId,
+ int offset,
+ int limit,
+ int totalCount,
+ List valuesList
+ ) {
+ Map values = new LinkedHashMap<>();
+ for (LookupValue lv : valuesList) {
+ values.put(lv.getKeyValue(), lv.getValueData());
+ }
+
+ Pagination pagination = new Pagination();
+ pagination.setOffset(offset);
+ pagination.setLimit(limit);
+ pagination.setHasMore(offset + limit < totalCount);
+
+ return new ExportGroupPagedResponse(groupId, totalCount, pagination, values, new Date());
+ }
+
+ // Getters and setters
+
+ public Integer getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(Integer groupId) {
+ this.groupId = groupId;
+ }
+
+ public Integer getTotalCount() {
+ return totalCount;
+ }
+
+ public void setTotalCount(Integer totalCount) {
+ this.totalCount = totalCount;
+ }
+
+ public Pagination getPagination() {
+ return pagination;
+ }
+
+ public void setPagination(Pagination pagination) {
+ this.pagination = pagination;
+ }
+
+ public Map getValues() {
+ return values;
+ }
+
+ public void setValues(Map values) {
+ this.values = values;
+ }
+
+ public Date getExportDate() {
+ return exportDate;
+ }
+
+ public void setExportDate(Date exportDate) {
+ this.exportDate = exportDate;
+ }
+
+ //Static inner class
+ public static class Pagination {
+ private int offset;
+ private int limit;
+ private boolean hasMore;
+
+ public int getOffset() {
+ return offset;
+ }
+
+ public void setOffset(int offset) {
+ this.offset = offset;
+ }
+
+ public int getLimit() {
+ return limit;
+ }
+
+ public void setLimit(int limit) {
+ this.limit = limit;
+ }
+
+ public boolean isHasMore() {
+ return hasMore;
+ }
+
+ public void setHasMore(boolean hasMore) {
+ this.hasMore = hasMore;
+ }
+ }
+}
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ExportLookupGroupResponse.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ExportLookupGroupResponse.java
new file mode 100644
index 000000000..12a1a69eb
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ExportLookupGroupResponse.java
@@ -0,0 +1,58 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupGroup;
+
+import java.util.Date;
+import java.util.Map;
+
+public class ExportLookupGroupResponse {
+ private LookupGroup group;
+ private Map values;
+ private Date exportDate;
+
+ public ExportLookupGroupResponse() {
+ }
+
+ public ExportLookupGroupResponse(LookupGroup group, Map values, Date exportDate) {
+ this.group = group;
+ this.values = values;
+ this.exportDate = exportDate;
+ }
+
+ public LookupGroup getGroup() {
+ return group;
+ }
+
+ public void setGroup(LookupGroup group) {
+ this.group = group;
+ }
+
+ public Map getValues() {
+ return values;
+ }
+
+ public void setValues(Map values) {
+ this.values = values;
+ }
+
+ public Date getExportDate() {
+ return exportDate;
+ }
+
+ public void setExportDate(Date exportDate) {
+ this.exportDate = exportDate;
+ }
+}
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/GroupAuditEntriesResponse.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/GroupAuditEntriesResponse.java
new file mode 100644
index 000000000..502c268ee
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/GroupAuditEntriesResponse.java
@@ -0,0 +1,217 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import com.mirth.connect.model.User;
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupAudit;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class GroupAuditEntriesResponse {
+ private int groupId;
+ private int totalEntries;
+ private List entries;
+ private Pagination pagination;
+
+ public static GroupAuditEntriesResponse fromResult(
+ int groupId,
+ List rawEntries,
+ int total,
+ int limit,
+ int offset,
+ List users
+ ) {
+ Map userIdToName = users.stream()
+ .collect(Collectors.toMap(
+ user -> String.valueOf(user.getId()),
+ User::getUsername
+ ));
+
+ // Ensure system user is included
+ userIdToName.putIfAbsent("0", "System");
+
+ List enrichedEntries = rawEntries.stream()
+ .map(audit -> {
+ AuditEntryResponse dto = new AuditEntryResponse();
+ dto.setId(audit.getId());
+ dto.setGroupId(audit.getGroupId());
+ dto.setKeyValue(audit.getKeyValue());
+ dto.setAction(audit.getAction());
+ dto.setOldValue(audit.getOldValue());
+ dto.setNewValue(audit.getNewValue());
+ dto.setTimestamp(audit.getTimestamp());
+ dto.setUserName(userIdToName.getOrDefault(audit.getUserId(), audit.getUserId()));
+ return dto;
+ })
+ .collect(Collectors.toList());
+
+ GroupAuditEntriesResponse response = new GroupAuditEntriesResponse();
+ response.groupId = groupId;
+ response.totalEntries = total;
+ response.entries = enrichedEntries;
+
+ Pagination pagination = new Pagination();
+ pagination.setLimit(limit);
+ pagination.setOffset(offset);
+ pagination.setHasMore((offset + enrichedEntries.size()) < total);
+ response.pagination = pagination;
+
+ return response;
+ }
+
+ // Getters and setters...
+
+ public int getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(int groupId) {
+ this.groupId = groupId;
+ }
+
+ public int getTotalEntries() {
+ return totalEntries;
+ }
+
+ public void setTotalEntries(int totalEntries) {
+ this.totalEntries = totalEntries;
+ }
+
+ public List getEntries() {
+ return entries;
+ }
+
+ public void setEntries(List entries) {
+ this.entries = entries;
+ }
+
+ public Pagination getPagination() {
+ return pagination;
+ }
+
+ public void setPagination(Pagination pagination) {
+ this.pagination = pagination;
+ }
+
+ // Inner class: AuditEntryResponse
+ public static class AuditEntryResponse {
+ private long id;
+ private int groupId;
+ private String keyValue;
+ private String action;
+ private String oldValue;
+ private String newValue;
+ private String userName;
+ private Date timestamp;
+
+ // Getters and setters...
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public int getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(int groupId) {
+ this.groupId = groupId;
+ }
+
+ public String getKeyValue() {
+ return keyValue;
+ }
+
+ public void setKeyValue(String keyValue) {
+ this.keyValue = keyValue;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public String getOldValue() {
+ return oldValue;
+ }
+
+ public void setOldValue(String oldValue) {
+ this.oldValue = oldValue;
+ }
+
+ public String getNewValue() {
+ return newValue;
+ }
+
+ public void setNewValue(String newValue) {
+ this.newValue = newValue;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(Date timestamp) {
+ this.timestamp = timestamp;
+ }
+ }
+
+ // Inner class: Pagination
+ public static class Pagination {
+ private int limit;
+ private int offset;
+ private boolean hasMore;
+
+ public int getLimit() {
+ return limit;
+ }
+
+ public void setLimit(int limit) {
+ this.limit = limit;
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+
+ public void setOffset(int offset) {
+ this.offset = offset;
+ }
+
+ public boolean isHasMore() {
+ return hasMore;
+ }
+
+ public void setHasMore(boolean hasMore) {
+ this.hasMore = hasMore;
+ }
+ }
+}
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/GroupStatisticsResponse.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/GroupStatisticsResponse.java
new file mode 100644
index 000000000..160587007
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/GroupStatisticsResponse.java
@@ -0,0 +1,86 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupStatistics;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Represents the overall lookup statistics and cache metrics
+ * for a specific lookup group, including database-level stats
+ * and runtime cache diagnostics.
+ */
+public class GroupStatisticsResponse {
+
+ private int groupId;
+ private long totalLookups;
+ private long cacheHits;
+ private double hitRate;
+ private Date lastAccessed;
+ private Date resetDate;
+ private CacheStatistics cacheStatistics;
+
+ // Getters
+ public int getGroupId() {
+ return groupId;
+ }
+
+ public long getTotalLookups() {
+ return totalLookups;
+ }
+
+ public long getCacheHits() {
+ return cacheHits;
+ }
+
+ public double getHitRate() {
+ return hitRate;
+ }
+
+ public Date getLastAccessed() {
+ return lastAccessed;
+ }
+
+ public Date getResetDate() {
+ return resetDate;
+ }
+
+ public CacheStatistics getCacheStatistics() {
+ return cacheStatistics;
+ }
+
+ /**
+ * Factory method to construct a full statistics response for a lookup group.
+ */
+ public static GroupStatisticsResponse fromResult(LookupStatistics stats, CacheStatistics cacheStatistics) {
+ GroupStatisticsResponse response = new GroupStatisticsResponse();
+
+ response.groupId = stats.getGroupId();
+ response.totalLookups = stats.getTotalLookups();
+ response.cacheHits = stats.getCacheHits();
+ response.hitRate = computeHitRate(stats.getCacheHits(), stats.getTotalLookups());
+ response.lastAccessed = stats.getLastAccessed();
+ response.resetDate = stats.getResetDate();
+ response.cacheStatistics = cacheStatistics;
+
+ return response;
+ }
+
+ private static double computeHitRate(long hits, long total) {
+ return (total > 0) ? (double) hits / total : 0.0;
+ }
+}
+
+
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ImportLookupGroupResponse.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ImportLookupGroupResponse.java
new file mode 100644
index 000000000..a5510fdba
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ImportLookupGroupResponse.java
@@ -0,0 +1,118 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ImportLookupGroupResponse {
+ public static final String STATUS_SUCCESS = "success";
+ public static final String STATUS_ERROR = "error";
+
+ private String status;
+ private int groupId;
+ private int importedCount;
+ private List errors;
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public int getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(int groupId) {
+ this.groupId = groupId;
+ }
+
+ public int getImportedCount() {
+ return importedCount;
+ }
+
+ public void setImportedCount(int importedCount) {
+ this.importedCount = importedCount;
+ }
+
+ public List getErrors() {
+ return errors;
+ }
+
+ public void setErrors(List errors) {
+ this.errors = errors;
+ }
+
+ public boolean isSuccessful() {
+ return STATUS_SUCCESS.equalsIgnoreCase(status);
+ }
+
+ public boolean isError() {
+ return STATUS_ERROR.equalsIgnoreCase(status);
+ }
+
+ public boolean hasErrors() {
+ return errors != null && !errors.isEmpty();
+ }
+
+ public static ImportLookupGroupResponse fromResult(int groupId, int importedCount, List errors) {
+ ImportLookupGroupResponse.Builder builder = new ImportLookupGroupResponse.Builder()
+ .withGroupId(groupId)
+ .withImportedCount(importedCount);
+
+ if (errors != null && !errors.isEmpty()) {
+ builder.addErrors(errors);
+ }
+
+ return builder.build();
+ }
+
+ public static class Builder {
+ private final ImportLookupGroupResponse response;
+
+ public Builder() {
+ response = new ImportLookupGroupResponse();
+ response.setErrors(new ArrayList<>());
+ }
+
+ public Builder withGroupId(int groupId) {
+ response.setGroupId(groupId);
+ return this;
+ }
+
+ public Builder withImportedCount(int count) {
+ response.setImportedCount(count);
+ return this;
+ }
+
+ public Builder addError(String errorMessage) {
+ response.getErrors().add(errorMessage);
+ return this;
+ }
+
+ public Builder addErrors(List errors) {
+ if (errors != null && !errors.isEmpty()) {
+ response.getErrors().addAll(errors);
+ }
+ return this;
+ }
+
+ public ImportLookupGroupResponse build() {
+ response.setStatus(ImportLookupGroupResponse.STATUS_SUCCESS);
+ return response;
+ }
+ }
+
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ImportValuesResponse.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ImportValuesResponse.java
new file mode 100644
index 000000000..d057f1be6
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/ImportValuesResponse.java
@@ -0,0 +1,63 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import java.util.List;
+
+public class ImportValuesResponse {
+ private Integer groupId;
+ private String status;
+ private int importedCount;
+ private List errors;
+
+ public ImportValuesResponse() {
+ }
+
+ public ImportValuesResponse(Integer groupId, String status, int importedCount, List errors) {
+ this.groupId = groupId;
+ this.status = status;
+ this.importedCount = importedCount;
+ this.errors = errors;
+ }
+
+ public Integer getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(Integer groupId) {
+ this.groupId = groupId;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public int getImportedCount() {
+ return importedCount;
+ }
+
+ public void setImportedCount(int importedCount) {
+ this.importedCount = importedCount;
+ }
+
+ public List getErrors() {
+ return errors;
+ }
+
+ public void setErrors(List errors) {
+ this.errors = errors;
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/LookupAllValuesResponse.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/LookupAllValuesResponse.java
new file mode 100644
index 000000000..ef4d9a858
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/LookupAllValuesResponse.java
@@ -0,0 +1,132 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+
+import java.util.List;
+
+public class LookupAllValuesResponse {
+ private int groupId;
+ private String groupName;
+ private int totalCount;
+ private List values;
+ private Pagination pagination;
+
+ // Constructors
+ public LookupAllValuesResponse() {
+ }
+
+ public LookupAllValuesResponse(int groupId, String groupName, int totalCount, List values, Pagination pagination) {
+ this.groupId = groupId;
+ this.groupName = groupName;
+ this.totalCount = totalCount;
+ this.values = values;
+ this.pagination = pagination;
+ }
+
+ // Getters and Setters
+ public int getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(int groupId) {
+ this.groupId = groupId;
+ }
+
+ public String getGroupName() {
+ return groupName;
+ }
+
+ public void setGroupName(String groupName) {
+ this.groupName = groupName;
+ }
+
+ public int getTotalCount() {
+ return totalCount;
+ }
+
+ public void setTotalCount(int totalCount) {
+ this.totalCount = totalCount;
+ }
+
+ public List getValues() {
+ return values;
+ }
+
+ public void setValues(List values) {
+ this.values = values;
+ }
+
+ public Pagination getPagination() {
+ return pagination;
+ }
+
+ public void setPagination(Pagination pagination) {
+ this.pagination = pagination;
+ }
+
+ public static LookupAllValuesResponse fromResult(
+ Integer groupId,
+ String groupName,
+ int totalCount,
+ List paginated,
+ int limit,
+ int offset
+ ) {
+ LookupAllValuesResponse response = new LookupAllValuesResponse();
+ response.setGroupId(groupId);
+ response.setGroupName(groupName);
+ response.setTotalCount(totalCount);
+ response.setValues(paginated);
+
+ Pagination pagination = new Pagination();
+ pagination.setLimit(limit);
+ pagination.setOffset(offset);
+ pagination.setHasMore(offset + limit < totalCount);
+ response.setPagination(pagination);
+
+ return response;
+ }
+
+ // ✅ Static inner class for pagination
+ public static class Pagination {
+ private int limit;
+ private int offset;
+ private boolean hasMore;
+
+ // Getters and setters
+ public int getLimit() {
+ return limit;
+ }
+
+ public void setLimit(int limit) {
+ this.limit = limit;
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+
+ public void setOffset(int offset) {
+ this.offset = offset;
+ }
+
+ public boolean isHasMore() {
+ return hasMore;
+ }
+
+ public void setHasMore(boolean hasMore) {
+ this.hasMore = hasMore;
+ }
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/LookupValueResponse.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/LookupValueResponse.java
new file mode 100644
index 000000000..8e71b7227
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/dto/response/LookupValueResponse.java
@@ -0,0 +1,93 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.dto.response;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.model.LookupValue;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import java.util.Date;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class LookupValueResponse {
+
+ private Integer groupId; // Optional: for context in the response
+ private String key;
+ private String value;
+ private Date createdDate;
+ private Date updatedDate;
+
+ public static LookupValueResponse from(LookupValue entity, Integer groupId, boolean full) {
+ if (full) {
+ return new LookupValueResponse(entity, groupId);
+ }
+ return new LookupValueResponse(entity.getKeyValue(), entity.getValueData(), null, null, groupId);
+ }
+
+ public LookupValueResponse() {
+ }
+
+ public LookupValueResponse(String key, String value, Date createdDate, Date updatedDate, Integer groupId) {
+ this.groupId = groupId;
+ this.key = key;
+ this.value = value;
+ this.createdDate = createdDate;
+ this.updatedDate = updatedDate;
+ }
+
+ public LookupValueResponse(LookupValue entity, Integer groupId) {
+ this.groupId = groupId;
+ this.key = entity.getKeyValue();
+ this.value = entity.getValueData();
+ this.createdDate = entity.getCreatedDate();
+ this.updatedDate = entity.getUpdatedDate();
+ }
+
+ // Getters and setters
+ public Integer getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(Integer groupId) {
+ this.groupId = groupId;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public Date getCreatedDate() {
+ return createdDate;
+ }
+
+ public void setCreatedDate(Date createdDate) {
+ this.createdDate = createdDate;
+ }
+
+ public Date getUpdatedDate() {
+ return updatedDate;
+ }
+
+ public void setUpdatedDate(Date updatedDate) {
+ this.updatedDate = updatedDate;
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/interfaces/LookupTableServletInterface.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/interfaces/LookupTableServletInterface.java
new file mode 100644
index 000000000..4db461e76
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/interfaces/LookupTableServletInterface.java
@@ -0,0 +1,884 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.interfaces;
+
+import com.mirth.connect.client.core.ClientException;
+import com.mirth.connect.client.core.api.BaseServletInterface;
+
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+
+import com.mirth.connect.client.core.api.MirthOperation;
+import com.mirth.connect.client.core.api.Param;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+/**
+ * @author Thai Tran (thaitran@innovarhealthcare.com)
+ * @create 2025-05-13 10:25 AM
+ */
+
+@Path("/v1/lookups")
+@Tag(name = "Lookup Table")
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+public interface LookupTableServletInterface extends BaseServletInterface {
+ public static final String PERMISSION_ACCESS = "Access Lookup Table";
+
+ @POST
+ @Path("/groups")
+ @Operation(summary = "Creates a new lookup group.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "The created lookup group returned as a JSON object.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "group",
+ summary = "Example response for a successfully created group",
+ value = "{\n" +
+ " \"id\": 3,\n" +
+ " \"name\": \"Provider Directory\",\n" +
+ " \"description\": \"Provider information lookup\",\n" +
+ " \"version\": \"1.0\",\n" +
+ " \"cacheSize\": 500,\n" +
+ " \"cachePolicy\": \"LRU\",\n" +
+ " \"createdDate\": \"2025-05-01T08:15:00Z\",\n" +
+ " \"updatedDate\": \"2025-05-01T08:15:00Z\"\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "createGroup", display = "Create new group", permission = PERMISSION_ACCESS)
+ public String createGroup(
+ @Param("requestBody")
+ @RequestBody(
+ description = "JSON object representing the lookup group to be created.",
+ required = true,
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "group",
+ summary = "group",
+ value = "{\n" +
+ " \"name\": \"Provider Directory\",\n" +
+ " \"description\": \"Provider information lookup\",\n" +
+ " \"version\": \"1.0\",\n" +
+ " \"cacheSize\": 500,\n" +
+ " \"cachePolicy\": \"LRU\"\n" +
+ "}"
+ )
+ )
+ )
+ String requestBody
+ ) throws ClientException;
+
+ @GET
+ @Path("/groups")
+ @Operation(summary = "Returns all lookup groups.")
+ @MirthOperation(name = "getAllGroups", display = "Get lookup group list", permission = PERMISSION_ACCESS)
+ public String getAllGroups() throws ClientException;
+
+ @PUT
+ @Path("/groups/{groupId}")
+ @Operation(summary = "Updates a specified group.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "Group was successfully updated.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "UpdatedGroup",
+ summary = "Updated group object",
+ value = "{\n" +
+ " \"id\": 3,\n" +
+ " \"name\": \"Provider Directory\",\n" +
+ " \"description\": \"Updated provider information lookup\",\n" +
+ " \"version\": \"1.1\",\n" +
+ " \"cacheSize\": 1000,\n" +
+ " \"cachePolicy\": \"LRU\",\n" +
+ " \"createdDate\": \"2025-05-01T08:15:00Z\",\n" +
+ " \"updatedDate\": \"2025-05-01T09:30:00Z\"\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "updateGroup", display = "Update group", permission = PERMISSION_ACCESS)
+ public String updateGroup(
+ @Param("groupId")
+ @Parameter(
+ name = "groupId",
+ description = "The unique id of the group to update.",
+ example = "1",
+ required = true)
+ @PathParam("groupId") Integer groupId,
+ @RequestBody(
+ description = "JSON object representing the lookup group to be update.",
+ required = true,
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "group",
+ summary = "group",
+ value = "{\n" +
+ " \"name\": \"Provider Directory\",\n" +
+ " \"description\": \"Updated provider information lookup\",\n" +
+ " \"version\": \"1.0\",\n" +
+ " \"cacheSize\": 500,\n" +
+ " \"cachePolicy\": \"LRU\"\n" +
+ "}"
+ )
+ )
+ )
+ String requestBody
+ ) throws ClientException;
+
+ @GET
+ @Path("/groups/{groupId}")
+ @Operation(summary = "Returns a specific lookup group by id.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "The lookup group returned as a JSON object.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "group",
+ value = "{\n" +
+ " \"id\": 3,\n" +
+ " \"name\": \"Provider Directory\",\n" +
+ " \"description\": \"Provider information lookup\",\n" +
+ " \"version\": \"1.0\",\n" +
+ " \"cacheSize\": 500,\n" +
+ " \"cachePolicy\": \"LRU\",\n" +
+ " \"createdDate\": \"2025-05-01T08:15:00Z\",\n" +
+ " \"updatedDate\": \"2025-05-01T08:15:00Z\"\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "getGroupById", display = "Get lookup group", permission = PERMISSION_ACCESS)
+ public String getGroupById(
+ @Param("groupId")
+ @Parameter(description = "The unique id of the group to retrieve.", required = true)
+ @PathParam("groupId") Integer groupId
+ ) throws ClientException;
+
+ @GET
+ @Path("/groups/name/{name}")
+ @Operation(summary = "Returns a specific lookup group by name.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "The lookup group returned as a JSON object.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "group",
+ value = "{\n" +
+ " \"id\": 3,\n" +
+ " \"name\": \"Provider Directory\",\n" +
+ " \"description\": \"Provider information lookup\",\n" +
+ " \"version\": \"1.0\",\n" +
+ " \"cacheSize\": 500,\n" +
+ " \"cachePolicy\": \"LRU\",\n" +
+ " \"createdDate\": \"2025-05-01T08:15:00Z\",\n" +
+ " \"updatedDate\": \"2025-05-01T08:15:00Z\"\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "getGroupByName", display = "Get lookup group", permission = PERMISSION_ACCESS)
+ public String getGroupByName(
+ @Param("name")
+ @Parameter(description = "The unique name of the group to retrieve.", required = true)
+ @PathParam("name") String name
+ ) throws ClientException;
+
+ @DELETE
+ @Path("/groups/{groupId}")
+ @Operation(summary = "Delete a specific group.")
+ @MirthOperation(name = "deleteGroup", display = "Delete group", permission = PERMISSION_ACCESS)
+ public void deleteGroup(
+ @Param("groupId")
+ @Parameter(description = "The unique id of the group to delete.", required = true)
+ @PathParam("groupId") Integer groupId
+ ) throws ClientException;
+
+ @GET
+ @Path("/groups/{groupId}/values")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Retrieves all values from a specific lookup group with optional pagination and key filtering.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "Successfully retrieved values",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "Successful response",
+ value = "{\n" +
+ " \"groupId\": 1,\n" +
+ " \"groupName\": \"Billing Codes\",\n" +
+ " \"totalCount\": 7,\n" +
+ " \"values\": [\n" +
+ " {\n" +
+ " \"keyValue\": \"99213\",\n" +
+ " \"valueData\": \"Office Visit\",\n" +
+ " \"createdDate\": \"2025-06-22T01:19:25.213+00:00\",\n" +
+ " \"updatedDate\": \"2025-06-22T01:29:45.123+00:00\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"keyValue\": \"99214\",\n" +
+ " \"valueData\": \"Office Visit, Level 4\",\n" +
+ " \"createdDate\": \"2025-06-22T01:19:25.213+00:00\",\n" +
+ " \"updatedDate\": \"2025-06-22T01:19:25.213+00:00\"\n" +
+ " }\n" +
+ " // truncated for brevity\n" +
+ " ],\n" +
+ " \"pagination\": {\n" +
+ " \"limit\": 100,\n" +
+ " \"offset\": 0,\n" +
+ " \"hasMore\": false\n" +
+ " }\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "getAllValues", display = "Retrieves all values", permission = PERMISSION_ACCESS)
+ public String getAllValues(
+ @Param("groupId")
+ @Parameter(description = "The unique ID of the group to retrieve.", required = true)
+ @PathParam("groupId") Integer groupId,
+
+ @Param("offset")
+ @Parameter(description = "Offset for pagination (default: 0)", required = false)
+ @QueryParam("offset") @DefaultValue("0") Integer offset,
+
+ @Param("limit")
+ @Parameter(description = "Maximum number of values to return (default: 100)", required = false)
+ @QueryParam("limit") @DefaultValue("100") Integer limit,
+
+ @Param("pattern")
+ @Parameter(description = "Filter keys by pattern (optional)", required = false)
+ @QueryParam("pattern") String pattern
+ ) throws ClientException;
+
+
+ @GET
+ @Path("/groups/{groupId}/values/{key}")
+ @Operation(summary = "Retrieve a lookup value by group and key.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "Successfully retrieved the lookup value.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "LookupValue",
+ summary = "A sample lookup value response",
+ value = "{\n" +
+ " \"groupId\": 1,\n" +
+ " \"key\": \"99213\",\n" +
+ " \"value\": \"Office Visit, Established Patient\",\n" +
+// " \"createdDate\": \"2025-05-28T15:17:12.504+00:00\",\n" +
+// " \"updatedDate\": \"2025-05-28T15:17:12.504+00:00\"\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "getValue", display = "Retrieve Lookup Value", permission = PERMISSION_ACCESS)
+ public String getValue(
+ @Param("groupId")
+ @Parameter(
+ name = "groupId",
+ description = "The unique ID of the lookup group containing the value.",
+ example = "1",
+ required = true
+ )
+ @PathParam("groupId") Integer groupId,
+
+ @Param("key")
+ @Parameter(
+ name = "key",
+ description = "The primary key of the lookup value within the specified group.",
+ example = "99213",
+ required = true
+ )
+ @PathParam("key") String key
+ ) throws ClientException;
+
+ @PUT
+ @Path("/groups/{groupId}/values/{key}")
+ @Operation(
+ summary = "Set or update the value for a specific key within a lookup group.",
+ description = "Updates the value associated with the specified key in the given lookup group. " +
+ "Expects a JSON body with a 'value' field. If the key exists, the value is updated; otherwise, a new entry may be created."
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "The lookup value returned as a JSON object.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "value",
+ value = "{\n" +
+ " \"groupId\": 1,\n" +
+ " \"key\": \"99213\",\n" +
+ " \"value\": \"Office Visit, Established Patient - Level 3\",\n" +
+ " \"createdDate\": \"2025-05-01T08:15:00Z\",\n" +
+ " \"updatedDate\": \"2025-05-01T08:15:00Z\"\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(
+ name = "setValue",
+ display = "Set Lookup Value",
+ permission = PERMISSION_ACCESS
+ )
+ public String setValue(
+ @Param("groupId")
+ @Parameter(
+ name = "groupId",
+ description = "The unique ID of the lookup group containing the key-value pair.",
+ example = "42",
+ required = true
+ )
+ @PathParam("groupId") Integer groupId,
+
+ @Param("key")
+ @Parameter(
+ name = "key",
+ description = "The primary key of the lookup value to update within the specified group.",
+ example = "99213",
+ required = true
+ )
+ @PathParam("key") String key,
+
+ @Param("requestBody")
+ @RequestBody(
+ description = "Raw JSON string containing a 'value' field.",
+ required = true,
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "value",
+ summary = "Set or update value",
+ value = "{ \"value\": \"Office Visit, Established Patient - Level 3\" }"
+ )
+ )
+ )
+ String requestBody
+
+ ) throws ClientException;
+
+ @DELETE
+ @Path("/groups/{groupId}/values/{key}")
+ @Operation(summary = "Delete a lookup value by group and key.")
+ @MirthOperation(name = "deleteValue", display = "Delete Lookup Value", permission = PERMISSION_ACCESS)
+ @Produces(MediaType.APPLICATION_JSON)
+ public void deleteValue(
+ @Param("groupId")
+ @Parameter(
+ name = "groupId",
+ description = "The unique ID of the lookup group containing the value.",
+ example = "42",
+ required = true
+ )
+ @PathParam("groupId") Integer groupId,
+
+ @Param("key")
+ @Parameter(
+ name = "key",
+ description = "The primary key of the lookup value within the specified group.",
+ example = "M",
+ required = true
+ )
+ @PathParam("key") String key
+ ) throws ClientException;
+
+ @POST
+ @Path("/groups/{groupId}/values")
+ @Operation(summary = "Imports key-value pairs into a specific lookup group.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "Import successful",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "Success",
+ value = "{\n" +
+ " \"groupId\": 1,\n" +
+ " \"status\": \"success\",\n" +
+ " \"importedCount\": 4,\n" +
+ " \"errors\": []\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "importValues", display = "Import lookup values", permission = PERMISSION_ACCESS)
+ public String importValues(
+ @Param("groupId")
+ @Parameter(description = "The ID of the group to import values into.", required = true)
+ @PathParam("groupId") Integer groupId,
+
+ @Param("clearExist")
+ @Parameter(description = "If true, existing values in the group will be cleared before import. Default: false", required = false)
+ @QueryParam("clearExist")
+ @DefaultValue("false") boolean clearExist,
+
+ @Param("requestBody")
+ @RequestBody(
+ description = "Raw JSON containing a 'values' object with key-value pairs to import.",
+ required = true,
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "ImportValuesExample",
+ summary = "Key-value import payload",
+ value = "{\n" +
+ " \"values\": {\n" +
+ " \"99213\": \"Office Visit, Established Patient\",\n" +
+ " \"99214\": \"Office Visit, Level 4\",\n" +
+ " \"99215\": \"Office Visit, Level 5\",\n" +
+ " \"J0696\": \"Injection, Ceftriaxone Sodium\"\n" +
+ " }\n" +
+ "}"
+ )
+ )
+ )
+ String requestBody
+ ) throws ClientException;
+
+ @GET
+ @Path("/groups/{groupId}/export")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Exports a lookup group and its values for backup or migration.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "Successfully exported the lookup group and its values.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "ExportExample",
+ summary = "Exported lookup group for backup/migration",
+ value = "{\n" +
+ " \"group\": {\n" +
+ " \"id\": 1,\n" +
+ " \"name\": \"Billing Codes\",\n" +
+ " \"description\": \"Standard billing codes for claims\",\n" +
+ " \"version\": \"1.0\",\n" +
+ " \"cacheSize\": 1000,\n" +
+ " \"cachePolicy\": \"LRU\",\n" +
+ " \"createdDate\": \"2024-12-15T08:00:00Z\",\n" +
+ " \"updatedDate\": \"2025-05-01T10:00:00Z\"\n" +
+ " },\n" +
+ " \"values\": {\n" +
+ " \"99213\": \"Office Visit, Established Patient\",\n" +
+ " \"99214\": \"Office Visit, Level 4\",\n" +
+ " \"99215\": \"Office Visit, Level 5\",\n" +
+ " \"J0696\": \"Injection, Ceftriaxone Sodium\"\n" +
+ " },\n" +
+ " \"exportDate\": \"2025-05-27T14:30:00Z\"\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "exportGroup", display = "Export Lookup Group", permission = PERMISSION_ACCESS)
+ public String exportGroup(
+ @Param("groupId")
+ @Parameter(description = "The ID of the lookup group to export.", required = true)
+ @PathParam("groupId") Integer groupId
+ ) throws ClientException;
+
+ @GET
+ @Path("/groups/{groupId}/exportPaged")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Exports values for a lookup group in pages, for large backups or migrations.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "Successfully exported a page of values for the specified lookup group.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "PagedExportExample",
+ summary = "A single page of lookup values",
+ value = "{\n" +
+ " \"groupId\": 1,\n" +
+ " \"offset\": 0,\n" +
+ " \"limit\": 10000,\n" +
+ " \"values\": {\n" +
+ " \"99213\": \"Office Visit, Established Patient\",\n" +
+ " \"99214\": \"Office Visit, Level 4\"\n" +
+ " }\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "exportGroupPaged", display = "Export Paged Lookup Group", permission = PERMISSION_ACCESS)
+ public String exportGroupPaged(
+ @Param("groupId")
+ @Parameter(description = "The ID of the lookup group to export.", required = true)
+ @PathParam("groupId") Integer groupId,
+
+ @QueryParam("offset")
+ @DefaultValue("0")
+ @Parameter(description = "Offset for pagination.")
+ Integer offset,
+
+ @QueryParam("limit")
+ @DefaultValue("10000")
+ @Parameter(description = "Maximum number of entries to return.")
+ Integer limit
+ ) throws ClientException;
+
+ @POST
+ @Path("/groups/import")
+ @Operation(summary = "Imports a lookup group and its values for migration or restore. If the group already exists, all of its existing values will be deleted and replaced with the new ones.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "Successfully imported the lookup group and its values.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "ImportExample",
+ summary = "Imported lookup group",
+ value = "{\n" +
+ " \"status\": \"success\",\n" +
+ " \"groupId\": 1,\n" +
+ " \"importedCount\": 4,\n" +
+ " \"errors\": []\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "importGroup", display = "Import Lookup Group", permission = PERMISSION_ACCESS)
+ public String importGroup(
+ @Param("updateIfExists")
+ @Parameter(description = "If true, updates the group if it already exists. Default: false", required = false)
+ @QueryParam("updateIfExists") @DefaultValue("false") boolean updateIfExists,
+
+ @Param("requestBody")
+ @RequestBody(
+ description = "Raw JSON containing a group and its values to import.",
+ required = true,
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "ImportRequestExample",
+ summary = "Import a group",
+ value = "{\n" +
+ " \"group\": {\n" +
+ " \"name\": \"Billing Codes\",\n" +
+ " \"description\": \"Standard billing codes for claims\",\n" +
+ " \"version\": \"1.0\",\n" +
+ " \"cacheSize\": 1000,\n" +
+ " \"cachePolicy\": \"LRU\"\n" +
+ " },\n" +
+ " \"values\": {\n" +
+ " \"99213\": \"Office Visit, Established Patient\",\n" +
+ " \"99214\": \"Office Visit, Level 4\",\n" +
+ " \"99215\": \"Office Visit, Level 5\",\n" +
+ " \"J0696\": \"Injection, Ceftriaxone Sodium\"\n" +
+ " }\n" +
+ "}"
+ )
+ )
+ )
+ String requestBody
+ ) throws ClientException;
+
+
+ @POST
+ @Path("/groups/{groupId}/values/batch")
+ @Operation(summary = "Batch Get Values", description = "Retrieves multiple values from a lookup group in a single request.")
+ @ApiResponse(
+ responseCode = "200",
+ description = "Successfully retrieved values for provided keys.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "BatchGetExample",
+ summary = "Successful lookup",
+ value = "{\n" +
+ " \"groupId\": 1,\n" +
+ " \"values\": {\n" +
+ " \"99213\": \"Office Visit, Established Patient\",\n" +
+ " \"99214\": \"Office Visit, Level 4\",\n" +
+ " \"99215\": \"Office Visit, Level 5\",\n" +
+ " \"J0696\": \"Injection, Ceftriaxone Sodium\"\n" +
+ " },\n" +
+ " \"missingKeys\": []\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "batchGetValues", display = "Batch Get Values", permission = PERMISSION_ACCESS)
+ public String batchGetValues(
+ @Param("groupId")
+ @Parameter(description = "The ID of the group to retrieve values from.", required = true)
+ @PathParam("groupId") Integer groupId,
+
+ @Param("requestBody")
+ @RequestBody(
+ description = "JSON body containing a list of keys to retrieve.",
+ required = true,
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "BatchGetRequest",
+ summary = "Keys to fetch",
+ value = "{ \"keys\": [\"99213\", \"99214\", \"99215\", \"J0696\"] }"
+ )
+ )
+ )
+ String requestBody
+ ) throws ClientException;
+
+ @GET
+ @Path("/groups/{groupId}/statistics")
+ @Operation(
+ summary = "Get Group Statistics",
+ description = "Retrieves usage and cache statistics for a specific lookup group."
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Successfully retrieved group statistics.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "GroupStatisticsExample",
+ summary = "Group usage and cache stats",
+ value = "{\n" +
+ " \"groupId\": 1,\n" +
+ " \"totalLookups\": 15423,\n" +
+ " \"cacheHits\": 12501,\n" +
+ " \"hitRate\": 0.81,\n" +
+ " \"lastAccessed\": \"2025-05-01T16:45:12Z\",\n" +
+ " \"resetDate\": \"2025-04-01T00:00:00Z\",\n" +
+ " \"cacheStatistics\": {\n" +
+ " \"size\": 267,\n" +
+ " \"maxSize\": 1000,\n" +
+ " \"hitCount\": 12501,\n" +
+ " \"missCount\": 2922,\n" +
+ " \"evictionCount\": 0\n" +
+ " }\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "getGroupStatistics", display = "Get Group Statistics", permission = PERMISSION_ACCESS)
+ public String getGroupStatistics(
+ @Param("groupId")
+ @Parameter(description = "The ID of the group to retrieve statistics for.", required = true)
+ @PathParam("groupId") Integer groupId
+ ) throws ClientException;
+
+ @POST
+ @Path("/groups/{groupId}/statistics/reset")
+ @Operation(
+ summary = "Reset Group Statistics",
+ description = "Resets total lookups and cache hits for a specific group, and updates the reset timestamp."
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Statistics successfully reset.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "ResetSuccessExample",
+ summary = "Successful statistics reset",
+ value = "{\n" +
+ " \"status\": \"success\",\n" +
+ " \"message\": \"Statistics reset successfully for group ID: 1\"\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "resetGroupStatistics", display = "Reset Group Statistics", permission = PERMISSION_ACCESS)
+ public String resetGroupStatistics(
+ @Param("groupId")
+ @Parameter(description = "The ID of the group whose statistics will be reset.", required = true)
+ @PathParam("groupId") Integer groupId
+ ) throws ClientException;
+
+ @GET
+ @Path("/groups/{groupId}/audit")
+ @Operation(
+ summary = "Get Group Audit Entries",
+ description = "Retrieves a paginated list of audit entries for a specific lookup group."
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Successfully retrieved audit entries.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "AuditEntriesExample",
+ summary = "Audit log with pagination",
+ value = "{\n" +
+ " \"groupId\": 1,\n" +
+ " \"totalEntries\": 1243,\n" +
+ " \"entries\": [...],\n" +
+ " \"pagination\": {\n" +
+ " \"limit\": 100,\n" +
+ " \"offset\": 0,\n" +
+ " \"hasMore\": true\n" +
+ " }\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "getGroupAuditEntries", display = "Get Group Audit Entries", permission = PERMISSION_ACCESS)
+ public String getGroupAuditEntries(
+ @Param("groupId")
+ @Parameter(description = "The ID of the group to retrieve audit entries for.", required = true)
+ @PathParam("groupId") Integer groupId,
+
+ @QueryParam("offset")
+ @DefaultValue("0")
+ @Parameter(description = "Offset for pagination.")
+ Integer offset,
+
+ @QueryParam("limit")
+ @DefaultValue("100")
+ @Parameter(description = "Maximum number of entries to return.")
+ Integer limit
+
+ ) throws ClientException;
+
+ @POST
+ @Path("/groups/{groupId}/audit/search")
+ @Operation(
+ summary = "Search Group Audit Entries",
+ description = "Retrieves a paginated list of audit entries for a specific group using filter criteria."
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Successfully retrieved audit entries.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "AuditEntriesExample",
+ summary = "Audit log with pagination",
+ value = "{\n" +
+ " \"groupId\": 1,\n" +
+ " \"totalEntries\": 1243,\n" +
+ " \"entries\": [...],\n" +
+ " \"pagination\": {\n" +
+ " \"limit\": 100,\n" +
+ " \"offset\": 0,\n" +
+ " \"hasMore\": true\n" +
+ " }\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "searchGroupAuditEntries", display = "Search Group Audit Entries", permission = PERMISSION_ACCESS)
+ public String searchGroupAuditEntries(
+ @Param("groupId")
+ @PathParam("groupId")
+ Integer groupId,
+
+ @QueryParam("offset")
+ @DefaultValue("0")
+ Integer offset,
+
+ @QueryParam("limit")
+ @DefaultValue("100")
+ Integer limit,
+
+ @Param("filterState")
+ @RequestBody(
+ description = "JSON object representing filter criteria for audit entry search.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "filter",
+ summary = "Example filter for audit entry search",
+ value = "{\n" +
+ " \"keyValue\": \"Provider\",\n" +
+ " \"action\": \"CREATE\",\n" +
+ " \"userId\": \"1\",\n" +
+ " \"startDate\": \"2025-06-01T00:00:00\",\n" +
+ " \"endDate\": \"2025-06-16T23:59:59\"\n" +
+ "}"
+ )
+ )
+ )
+ String filterState
+ ) throws ClientException;
+
+
+ @POST
+ @Path("/groups/{groupId}/cache/clear")
+ @Operation(
+ summary = "Clear Group Cache",
+ description = "Clears the in-memory cache for a specific lookup group."
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Cache cleared successfully.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "SuccessResponse",
+ summary = "Successful cache clear",
+ value = "{\n" +
+ " \"status\": \"success\",\n" +
+ " \"message\": \"Cache cleared successfully for group ID: 1\"\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "clearGroupCache", display = "Clear Group Cache", permission = PERMISSION_ACCESS)
+ public String clearGroupCache(
+ @Param("groupId")
+ @Parameter(description = "The ID of the group to clear the cache for.", required = true)
+ @PathParam("groupId") Integer groupId
+ ) throws ClientException;
+
+ @POST
+ @Path("/cache/clear")
+ @Operation(
+ summary = "Clear All Caches",
+ description = "Clears the in-memory caches for all lookup groups."
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "All caches cleared successfully.",
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ examples = @ExampleObject(
+ name = "SuccessResponse",
+ summary = "All caches cleared",
+ value = "{\n" +
+ " \"status\": \"success\",\n" +
+ " \"message\": \"All caches cleared successfully\"\n" +
+ "}"
+ )
+ )
+ )
+ @MirthOperation(name = "clearAllCaches", display = "Clear All Caches", permission = PERMISSION_ACCESS)
+ public String clearAllCaches() throws ClientException;
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/HistoryFilterState.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/HistoryFilterState.java
new file mode 100644
index 000000000..57327e2c4
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/HistoryFilterState.java
@@ -0,0 +1,129 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.model;
+
+import com.mirth.connect.plugins.dynamiclookup.shared.util.JsonUtils;
+
+import java.util.Date;
+import java.util.Objects;
+
+public class HistoryFilterState {
+ private String keyValue;
+ private String action;
+ private String userId;
+ private Date startDate;
+ private Date endDate;
+
+ // Constructors
+ public HistoryFilterState() {
+ }
+
+ public HistoryFilterState(String keyValue, String action, String userId, Date startDate, Date endDate) {
+ this.keyValue = keyValue;
+ this.action = action;
+ this.userId = userId;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ }
+
+ // Getters and Setters
+ public String getKeyValue() {
+ return keyValue;
+ }
+
+ public void setKeyValue(String keyValue) {
+ this.keyValue = keyValue;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public Date getStartDate() {
+ return startDate;
+ }
+
+ public void setStartDate(Date startDate) {
+ this.startDate = startDate;
+ }
+
+ public Date getEndDate() {
+ return endDate;
+ }
+
+ public void setEndDate(Date endDate) {
+ this.endDate = endDate;
+ }
+
+ // Check if all fields are empty
+ public boolean isEmpty() {
+ return isNullOrEmpty(keyValue) &&
+ isNullOrEmpty(action) &&
+ isNullOrEmpty(userId) &&
+ startDate == null &&
+ endDate == null;
+ }
+
+ private boolean isNullOrEmpty(String s) {
+ return s == null || s.trim().isEmpty();
+ }
+
+ // Delegate JSON methods to JsonUtils
+ public String toJson() throws Exception {
+ return JsonUtils.toJson(this);
+ }
+
+ public static HistoryFilterState fromJson(String json) throws Exception {
+ return JsonUtils.fromJson(json, HistoryFilterState.class);
+ }
+
+ @Override
+ public String toString() {
+ return "HistoryFilterState{" +
+ "keyValue='" + keyValue + '\'' +
+ ", action='" + action + '\'' +
+ ", userId='" + userId + '\'' +
+ ", startDate=" + startDate +
+ ", endDate=" + endDate +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof HistoryFilterState)) return false;
+ HistoryFilterState that = (HistoryFilterState) o;
+ return Objects.equals(keyValue, that.keyValue) &&
+ Objects.equals(action, that.action) &&
+ Objects.equals(userId, that.userId) &&
+ Objects.equals(startDate, that.startDate) &&
+ Objects.equals(endDate, that.endDate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyValue, action, userId, startDate, endDate);
+ }
+}
+
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupAudit.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupAudit.java
new file mode 100644
index 000000000..f774af7a9
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupAudit.java
@@ -0,0 +1,101 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.model;
+
+import java.util.Date;
+
+/**
+ * @author Thai Tran (thaitran@innovarhealthcare.com)
+ * @create 2025-05-13 10:25 AM
+ */
+public class LookupAudit {
+ private long id;
+ private int groupId;
+ private String tableName;
+ private String keyValue;
+ private String action; // "CREATE", "UPDATE", "DELETE", "IMPORT"
+ private String oldValue;
+ private String newValue;
+ private String userId;
+ private Date timestamp;
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public int getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(int groupId) {
+ this.groupId = groupId;
+ }
+
+ public String getTableName() {
+ return tableName;
+ }
+
+ public void setTableName(String tableName) {
+ this.tableName = tableName;
+ }
+
+ public String getKeyValue() {
+ return keyValue;
+ }
+
+ public void setKeyValue(String keyValue) {
+ this.keyValue = keyValue;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public String getOldValue() {
+ return oldValue;
+ }
+
+ public void setOldValue(String oldValue) {
+ this.oldValue = oldValue;
+ }
+
+ public String getNewValue() {
+ return newValue;
+ }
+
+ public void setNewValue(String newValue) {
+ this.newValue = newValue;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(Date timestamp) {
+ this.timestamp = timestamp;
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupGroup.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupGroup.java
new file mode 100644
index 000000000..506cae70a
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupGroup.java
@@ -0,0 +1,112 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.model;
+
+import java.util.Date;
+
+/**
+ * @author Thai Tran (thaitran@innovarhealthcare.com)
+ * @create 2025-05-13 10:25 AM
+ */
+public class LookupGroup {
+ private int id;
+ private String name;
+ private String description;
+ private String version;
+ private int cacheSize; // Default: 1000
+ private String cachePolicy; // "LRU" or "FIFO"
+ private Date createdDate;
+ private Date updatedDate;
+
+ public LookupGroup() {
+ id = 0;
+ name = "";
+ description = "";
+ version = "";
+ cacheSize = 1000;
+ cachePolicy = "LRU";
+ }
+
+ public LookupGroup(LookupGroup other) {
+ this.id = other.id;
+ this.name = other.name;
+ this.description = other.description;
+ this.version = other.version;
+ this.cacheSize = other.cacheSize;
+ this.cachePolicy = other.cachePolicy;
+ this.createdDate = other.createdDate != null ? new Date(other.createdDate.getTime()) : null;
+ this.updatedDate = other.updatedDate != null ? new Date(other.updatedDate.getTime()) : null;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public int getCacheSize() {
+ return cacheSize;
+ }
+
+ public void setCacheSize(int cacheSize) {
+ this.cacheSize = cacheSize;
+ }
+
+ public String getCachePolicy() {
+ return cachePolicy;
+ }
+
+ public void setCachePolicy(String cachePolicy) {
+ this.cachePolicy = cachePolicy;
+ }
+
+ public Date getCreatedDate() {
+ return createdDate;
+ }
+
+ public void setCreatedDate(Date createdDate) {
+ this.createdDate = createdDate;
+ }
+
+ public Date getUpdatedDate() {
+ return updatedDate;
+ }
+
+ public void setUpdatedDate(Date updatedDate) {
+ this.updatedDate = updatedDate;
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupStatistics.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupStatistics.java
new file mode 100644
index 000000000..0615a6719
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupStatistics.java
@@ -0,0 +1,65 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.model;
+
+import java.util.Date;
+
+/**
+ * @author Thai Tran (thaitran@innovarhealthcare.com)
+ * @create 2025-05-13 10:25 AM
+ */
+public class LookupStatistics {
+ private int groupId;
+ private long totalLookups;
+ private long cacheHits;
+ private Date lastAccessed;
+ private Date resetDate;
+
+ public int getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(int groupId) {
+ this.groupId = groupId;
+ }
+
+ public long getTotalLookups() {
+ return totalLookups;
+ }
+
+ public void setTotalLookups(long totalLookups) {
+ this.totalLookups = totalLookups;
+ }
+
+ public long getCacheHits() {
+ return cacheHits;
+ }
+
+ public void setCacheHits(long cacheHits) {
+ this.cacheHits = cacheHits;
+ }
+
+ public Date getLastAccessed() {
+ return lastAccessed;
+ }
+
+ public void setLastAccessed(Date lastAccessed) {
+ this.lastAccessed = lastAccessed;
+ }
+
+ public Date getResetDate() {
+ return resetDate;
+ }
+
+ public void setResetDate(Date resetDate) {
+ this.resetDate = resetDate;
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupValue.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupValue.java
new file mode 100644
index 000000000..0c9a57d7f
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/model/LookupValue.java
@@ -0,0 +1,73 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.model;
+
+import java.util.Date;
+
+/**
+ * @author Thai Tran (thaitran@innovarhealthcare.com)
+ * @create 2025-05-13 10:25 AM
+ */
+public class LookupValue {
+ private String keyValue;
+ private String valueData;
+ private Date createdDate;
+ private Date updatedDate;
+
+ public LookupValue() {
+ keyValue = "";
+ valueData = "";
+ }
+
+ public LookupValue(String keyValue, String valueData) {
+ this.keyValue = keyValue;
+ this.valueData = valueData;
+ }
+
+ public LookupValue(LookupValue other) {
+ keyValue = other.keyValue;
+ valueData = other.valueData;
+ this.createdDate = other.createdDate != null ? new Date(other.createdDate.getTime()) : null;
+ this.updatedDate = other.updatedDate != null ? new Date(other.updatedDate.getTime()) : null;
+ }
+
+ public String getKeyValue() {
+ return keyValue;
+ }
+
+ public void setKeyValue(String keyValue) {
+ this.keyValue = keyValue;
+ }
+
+ public String getValueData() {
+ return valueData;
+ }
+
+ public void setValueData(String valueData) {
+ this.valueData = valueData;
+ }
+
+ public Date getCreatedDate() {
+ return createdDate;
+ }
+
+ public void setCreatedDate(Date createdDate) {
+ this.createdDate = createdDate;
+ }
+
+ public Date getUpdatedDate() {
+ return updatedDate;
+ }
+
+ public void setUpdatedDate(Date updatedDate) {
+ this.updatedDate = updatedDate;
+ }
+}
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/util/JsonUtils.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/util/JsonUtils.java
new file mode 100644
index 000000000..9f649220b
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/util/JsonUtils.java
@@ -0,0 +1,69 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.util.StdDateFormat;
+
+import java.util.List;
+
+public class JsonUtils {
+
+ private static final ObjectMapper mapper = createDefaultMapper();
+
+ private static ObjectMapper createDefaultMapper() {
+ ObjectMapper objectMapper = new ObjectMapper();
+
+ // Handle ISO 8601 date format (e.g., 2025-05-20T15:30:00Z)
+ objectMapper.setDateFormat(new StdDateFormat().withColonInTimeZone(true));
+ objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+ // Optional: fail fast on unknown properties
+ objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+
+ // Optional: skip nulls in output
+ // objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+
+ // Optional: readable output
+ // objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
+
+ return objectMapper;
+ }
+
+ public static ObjectMapper getMapper() {
+ return mapper;
+ }
+
+ public static T fromJson(String json, Class clazz) throws Exception {
+ return mapper.readValue(json, clazz);
+ }
+
+ public static String toJson(Object object) throws Exception {
+ return mapper.writeValueAsString(object);
+ }
+
+ public static String toJsonPretty(Object object) throws Exception {
+ return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+ }
+
+ public static List fromJsonList(String json, Class clazz) {
+ try {
+ return mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, clazz));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to deserialize JSON list", e);
+ }
+ }
+}
+
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/util/LookupErrorCode.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/util/LookupErrorCode.java
new file mode 100644
index 000000000..39b9b5793
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/util/LookupErrorCode.java
@@ -0,0 +1,26 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.util;
+
+public final class LookupErrorCode {
+
+ private LookupErrorCode() {
+ // prevent instantiation
+ }
+
+ public static final String INVALID_REQUEST = "INVALID_REQUEST"; // Malformed request
+ public static final String GROUP_NOT_FOUND = "GROUP_NOT_FOUND"; // Lookup group not found
+ public static final String VALUE_NOT_FOUND = "VALUE_NOT_FOUND"; // Lookup value not found
+ public static final String DUPLICATE_GROUP_NAME = "DUPLICATE_GROUP_NAME"; // Group name already exists
+ public static final String INVALID_KEY = "INVALID_KEY"; // Invalid key format or length
+ public static final String DATABASE_ERROR = "DATABASE_ERROR"; // DB operation failed
+ public static final String PERMISSION_DENIED = "PERMISSION_DENIED"; // Insufficient permissions
+}
\ No newline at end of file
diff --git a/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/util/TtlUtils.java b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/util/TtlUtils.java
new file mode 100644
index 000000000..c0232ed75
--- /dev/null
+++ b/custom-extensions/dynamic-lookup-gateway/shared/src/main/java/com/mirth/connect/plugins/dynamiclookup/shared/util/TtlUtils.java
@@ -0,0 +1,35 @@
+/*
+ *
+ * Copyright (c) Innovar Healthcare. All rights reserved.
+ *
+ * https://www.innovarhealthcare.com
+ *
+ * The software in this package is published under the terms of the MPL license a copy of which has
+ * been included with this distribution in the LICENSE.txt file.
+ */
+
+package com.mirth.connect.plugins.dynamiclookup.shared.util;
+
+import java.util.Date;
+
+public class TtlUtils {
+
+ /**
+ * Checks if the given updatedAt timestamp is within the allowed TTL window.
+ *
+ * @param updatedAt the timestamp to check
+ * @param ttlHours TTL in hours (0 or less means no TTL enforced)
+ * @return true if updatedAt is within TTL or TTL is not enforced
+ */
+ public static boolean isWithinTtl(Date updatedAt, long ttlHours) {
+ if (updatedAt == null || ttlHours <= 0) {
+ return true;
+ }
+
+ long now = System.currentTimeMillis();
+ long cutoff = now - (ttlHours * 3600 * 1000L);
+
+ return updatedAt.getTime() >= cutoff;
+ }
+}
+
diff --git a/server/mirth-build.properties b/server/mirth-build.properties
index 6f0929a3a..34a1f551c 100644
--- a/server/mirth-build.properties
+++ b/server/mirth-build.properties
@@ -4,4 +4,5 @@ client=../client
webadmin=../webadmin
manager=../manager
cli=../command
+custom-extensions=../custom-extensions
version=4.5.4
diff --git a/server/mirth-build.xml b/server/mirth-build.xml
index 1b4976042..56c9326ce 100644
--- a/server/mirth-build.xml
+++ b/server/mirth-build.xml
@@ -193,8 +193,8 @@
-
-
-
+
+
+