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: + *
    + *
  1. Attempt to retrieve from cache.
  2. + *
  3. If not present or stale (based on TTL), load from the database.
  4. + *
  5. If loaded from the database, cache the value (along with its updatedAt timestamp).
  6. + *
  7. If the database value is also stale (based on TTL), return null.
  8. + *
+ * + * @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: + *

    + *
  1. Try to retrieve from cache.
  2. + *
  3. If not found or stale (based on TTL), load from the database and cache it.
  4. + *
  5. If the database value is also stale, it is excluded from the result.
  6. + *
+ * 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 @@ - - - + + +