diff --git a/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowser.java b/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowser.java index 18d3c1b02..abbbe2638 100644 --- a/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowser.java +++ b/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowser.java @@ -150,6 +150,7 @@ public class MessageBrowser extends javax.swing.JPanel { private String channelId; protected List channelIds = new ArrayList(); private String channelName; + private MessageBrowserRecentFilterStore recentFilterStore; private boolean isChannelDeployed; private boolean isCURESPHILoggingOn; protected boolean isChannelMessagesPanelFirstLoadSearch; @@ -331,6 +332,7 @@ public void loadChannel(MessageBrowserChannelModel channelModel) { this.channelId = channelId; this.channelName = channelName; + this.recentFilterStore = new MessageBrowserRecentFilterStore(channelId); this.connectors = connectors; this.connectors.put(null, "Deleted Connectors"); initMetaDataColumns(channelModel); @@ -340,6 +342,7 @@ public void loadChannel(MessageBrowserChannelModel channelModel) { resetSearchCriteria(); advancedSearchPopup.setSelectedMetaDataIds(selectedMetaDataIds); updateAdvancedSearchButtonFont(); + recentFiltersButton.setEnabled(!recentFilterStore.getRecentFilters().isEmpty()); lastUserSelectedMessageType = "Raw"; updateMessageRadioGroup(); @@ -452,6 +455,75 @@ public void updateAdvancedSearchButtonFont() { } } + private void restoreRecentFilter() { + // Multi-channel browse may be null + if (recentFilterStore == null) return; + + JPopupMenu popupMenu = new JPopupMenu(); + for (MessageFilter filter : recentFilterStore.getRecentFilters()) { + var menuItem = new JMenuItem(filter.toDisplayString(connectors, "; ", /* includeEmptyCriteria: */ false)); + menuItem.addActionListener(e -> applyMessageFilter(filter)); + popupMenu.add(menuItem); + } + popupMenu.show(recentFiltersButton, 0, recentFiltersButton.getHeight()); + } + + private void applyMessageFilter(MessageFilter filter) { + resetSearchCriteria(); + + boolean allDay = inferAllDay(filter); + + mirthDatePicker1.setDate((filter.getStartDate() == null) ? null : filter.getStartDate().getTime()); + mirthDatePicker2.setDate((filter.getEndDate() == null) ? null : filter.getEndDate().getTime()); + allDayCheckBox.setSelected(allDay); + mirthTimePicker1.setEnabled(mirthDatePicker1.getDate() != null && !allDay); + mirthTimePicker2.setEnabled(mirthDatePicker2.getDate() != null && !allDay); + + if (filter.getStartDate() != null) { + mirthTimePicker1.setDate(new SimpleDateFormat("HH:mm").format(filter.getStartDate().getTime())); + } + if (filter.getEndDate() != null && !allDay) { + mirthTimePicker2.setDate(new SimpleDateFormat("HH:mm").format(filter.getEndDate().getTime())); + } + + textSearchField.setText(StringUtils.defaultString(filter.getTextSearch())); + regexTextSearchCheckBox.setSelected(Boolean.TRUE.equals(filter.getTextSearchRegex())); + + Set statuses = filter.getStatuses(); + statusBoxReceived.setSelected(statuses != null && statuses.contains(Status.RECEIVED)); + statusBoxTransformed.setSelected(statuses != null && statuses.contains(Status.TRANSFORMED)); + statusBoxFiltered.setSelected(statuses != null && statuses.contains(Status.FILTERED)); + statusBoxQueued.setSelected(statuses != null && statuses.contains(Status.QUEUED)); + statusBoxPending.setSelected(statuses != null && statuses.contains(Status.PENDING)); + statusBoxSent.setSelected(statuses != null && statuses.contains(Status.SENT)); + statusBoxError.setSelected(statuses != null && statuses.contains(Status.ERROR)); + + advancedSearchPopup.applyFilter(filter); + updateAdvancedSearchButtonFont(); + updateFilterButtonFont(Font.BOLD); + } + + private boolean inferAllDay(MessageFilter filter) { + if (filter == null) { + return false; + } + + Calendar endDate = filter.getEndDate(); + if (endDate != null) { + return endDate.get(Calendar.HOUR_OF_DAY) == 23 + && endDate.get(Calendar.MINUTE) == 59 + && endDate.get(Calendar.SECOND) == 59 + && endDate.get(Calendar.MILLISECOND) == 999; + } + + Calendar startDate = filter.getStartDate(); + return startDate != null + && startDate.get(Calendar.HOUR_OF_DAY) == 0 + && startDate.get(Calendar.MINUTE) == 0 + && startDate.get(Calendar.SECOND) == 0 + && startDate.get(Calendar.MILLISECOND) == 0; + } + public String getChannelId() { return channelId; } @@ -654,6 +726,13 @@ protected boolean generateMessageFilter() { advancedSearchPopup.applySelectionsToFilter(messageFilter); selectedMetaDataIds = messageFilter.getIncludedMetaDataIds(); + if (recentFilterStore != null && !messageFilter.isEmpty()) { + recentFilterStore.addRecentFilter(messageFilter); + recentFiltersButton.setEnabled(true); + } + + // To keep page results consistent, the search is "capped" to the most + // recent message that has been received by the channel. if (messageFilter.getMaxMessageId() == null) { try { Long maxMessageId = parent.mirthClient.getMaxMessageId(channelId); @@ -684,7 +763,7 @@ protected void runSearch() { clearCache(); loadPageNumber(1); - updateSearchCriteriaPane(); + lastSearchCriteria.setText(messageFilter.toDisplayString(connectors, "\n", /* includeEmptyCriteria: */ true)); auditSearch(); } } @@ -723,183 +802,6 @@ protected void auditSearch(String channelId, String channelName) { } } - protected void updateSearchCriteriaPane() { - StringBuilder text = new StringBuilder(); - Calendar startDate = messageFilter.getStartDate(); - Calendar endDate = messageFilter.getEndDate(); - String padding = "\n"; - - text.append("Max Message Id: "); - text.append(messageFilter.getMaxMessageId()); - - if (messageFilter.getMinMessageId() != null) { - text.append(padding + "Min Message Id: "); - text.append(messageFilter.getMinMessageId()); - } - - String startDateFormatString = mirthTimePicker1.isEnabled() ? "yyyy-MM-dd HH:mm" : "yyyy-MM-dd"; - String endDateFormatString = mirthTimePicker2.isEnabled() ? "yyyy-MM-dd HH:mm" : "yyyy-MM-dd"; - - DateFormat startDateFormat = new SimpleDateFormat(startDateFormatString); - DateFormat endDateFormat = new SimpleDateFormat(endDateFormatString); - - text.append(padding + "Date Range: "); - - if (startDate == null) { - text.append("(any)"); - } else { - text.append(startDateFormat.format(startDate.getTime())); - if (!mirthTimePicker1.isEnabled()) { - text.append(" (all day)"); - } - } - - text.append(" to "); - - if (endDate == null) { - text.append("(any)"); - } else { - text.append(endDateFormat.format(endDate.getTime())); - if (!mirthTimePicker2.isEnabled()) { - text.append(" (all day)"); - } - } - - text.append(padding + "Statuses: "); - - if (messageFilter.getStatuses() == null) { - text.append("(any)"); - } else { - text.append(StringUtils.join(messageFilter.getStatuses(), ", ")); - } - - if (messageFilter.getTextSearch() != null) { - text.append(padding + "Text Search: " + messageFilter.getTextSearch()); - } - - text.append(getConnectorSearchCriteriaText(padding)); - - if (messageFilter.getOriginalIdLower() != null || messageFilter.getOriginalIdUpper() != null) { - text.append(padding + "Original Id: "); - if (messageFilter.getOriginalIdUpper() == null) { - text.append("Greater than " + messageFilter.getOriginalIdLower()); - } else if (messageFilter.getOriginalIdLower() == null) { - text.append("Less than " + messageFilter.getOriginalIdUpper()); - } else { - text.append("Between " + messageFilter.getOriginalIdLower() + " and " + messageFilter.getOriginalIdUpper()); - } - } - - if (messageFilter.getImportIdLower() != null || messageFilter.getImportIdUpper() != null) { - text.append(padding + "Import Id: "); - if (messageFilter.getImportIdUpper() == null) { - text.append("Greater than " + messageFilter.getImportIdLower()); - } else if (messageFilter.getImportIdLower() == null) { - text.append("Less than " + messageFilter.getImportIdUpper()); - } else { - text.append("Between " + messageFilter.getImportIdLower() + " and " + messageFilter.getImportIdUpper()); - } - } - - if (messageFilter.getServerId() != null) { - text.append(padding + "Server Id: " + messageFilter.getServerId()); - } - - Integer sendAttemptsLower = messageFilter.getSendAttemptsLower(); - Integer sendAttemptsUpper = messageFilter.getSendAttemptsUpper(); - - if (sendAttemptsLower != null || sendAttemptsUpper != null) { - text.append(padding + "# of Send Attempts: "); - - if (sendAttemptsLower != null) { - text.append(sendAttemptsLower); - } else { - text.append("(any)"); - } - - text.append(" - "); - - if (sendAttemptsUpper != null) { - text.append(sendAttemptsUpper); - } else { - text.append("(any)"); - } - } - - if (messageFilter.getContentSearch() != null) { - List contentSearch = messageFilter.getContentSearch(); - - for (ContentSearchElement element : contentSearch) { - for (String value : element.getSearches()) { - text.append(padding + ContentType.fromCode(element.getContentCode()) + " contains \"" + value + "\""); - } - } - } - - if (messageFilter.getMetaDataSearch() != null) { - List elements = messageFilter.getMetaDataSearch(); - - for (MetaDataSearchElement element : elements) { - text.append(padding + element.getColumnName() + " " + MetaDataSearchOperator.fromString(element.getOperator()).toString() + " "); - if (element.getValue() instanceof Calendar) { - Calendar date = (Calendar) element.getValue(); - text.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date.getTime())); - } else { - text.append(element.getValue()); - } - if (element.getIgnoreCase()) { - text.append(" (Ignore Case)"); - } - } - } - - if (messageFilter.getAttachment()) { - text.append(padding + "Has Attachment"); - } - - if (messageFilter.getError()) { - text.append(padding + "Has Error"); - } - - lastSearchCriteria.setText(text.toString()); - } - - protected String getConnectorSearchCriteriaText(String padding) { - StringBuilder text = new StringBuilder(); - text.append(padding + "Connectors: "); - - if (messageFilter.getIncludedMetaDataIds() == null) { - if (messageFilter.getExcludedMetaDataIds() == null) { - text.append("(any)"); - } else { - List excludedMetaDataIds = messageFilter.getExcludedMetaDataIds(); - List connectorNames = new ArrayList(); - - for (Entry connectorEntry : connectors.entrySet()) { - if (!excludedMetaDataIds.contains(connectorEntry.getKey())) { - connectorNames.add(connectorEntry.getValue()); - } - } - - text.append(StringUtils.join(connectorNames, ", ")); - } - } else if (messageFilter.getIncludedMetaDataIds().isEmpty()) { - text.append("(none)"); - } else { - List includedMetaDataIds = messageFilter.getIncludedMetaDataIds(); - List connectorNames = new ArrayList(); - - for (Entry connectorEntry : connectors.entrySet()) { - if (includedMetaDataIds.contains(connectorEntry.getKey())) { - connectorNames.add(connectorEntry.getValue()); - } - } - - text.append(StringUtils.join(connectorNames, ", ")); - } - return text.toString(); - } - public void jumpToPageNumber() { if (messages.getPageCount() != null && messages.getPageCount() > 0 && StringUtils.isNotEmpty(pageNumberField.getText())) { loadPageNumber(Math.min(Math.max(Integer.parseInt(pageNumberField.getText()), 1), messages.getPageCount())); @@ -2825,6 +2727,11 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { } }); + recentFiltersButton = new javax.swing.JButton(); + recentFiltersButton.setText("Recent..."); + recentFiltersButton.setEnabled(false); + recentFiltersButton.addActionListener(e -> restoreRecentFilter()); + statusBoxFiltered.setBackground(new java.awt.Color(255, 255, 255)); statusBoxFiltered.setText("FILTERED"); statusBoxFiltered.setFont(new java.awt.Font("Lucida Grande", 0, 11)); // NOI18N @@ -2952,8 +2859,9 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(allDayCheckBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(filterButton, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(regexTextSearchCheckBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(recentFiltersButton, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(regexTextSearchCheckBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(filterButton, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(statusBoxQueued, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) @@ -3007,7 +2915,8 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { .addComponent(mirthTimePicker2, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(mirthDatePicker2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(jLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(jLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(recentFiltersButton)) .addGap(7, 7, 7) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(textSearchField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) @@ -3287,6 +3196,7 @@ private void textSearchFieldActionPerformed(java.awt.event.ActionEvent evt) {//G private javax.swing.JLabel processedResponseLabel; private javax.swing.JLabel processedResponseStatusLabel; private com.mirth.connect.client.ui.components.MirthSyntaxTextArea processedResponseStatusTextArea; + private javax.swing.JButton recentFiltersButton; private com.mirth.connect.client.ui.components.MirthCheckBox regexTextSearchCheckBox; private javax.swing.JButton resetButton; private javax.swing.JLabel responseLabel; diff --git a/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowserAdvancedFilter.java b/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowserAdvancedFilter.java index f99d71e7f..8f0546d89 100644 --- a/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowserAdvancedFilter.java +++ b/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowserAdvancedFilter.java @@ -510,6 +510,81 @@ public Boolean hasAdvancedCriteria() { return hasAdvancedCriteria; } + public void applyFilter(MessageFilter messageFilter) { + stopEditing(); + resetSelections(); + + if (messageFilter == null) { + return; + } + + ItemSelectionTableModel connectorModel = ((ItemSelectionTableModel) connectorTable.getModel()); + DefaultTableModel contentSearchModel = ((DefaultTableModel) contentSearchTable.getModel()); + DefaultTableModel metaDataSearchModel = ((DefaultTableModel) metaDataSearchTable.getModel()); + + List includedMetaDataIds = messageFilter.getIncludedMetaDataIds(); + List excludedMetaDataIds = messageFilter.getExcludedMetaDataIds(); + + if (includedMetaDataIds != null) { + connectorModel.unselectAllKeys(); + for (Integer metaDataId : includedMetaDataIds) { + connectorModel.selectKey(metaDataId); + } + } else if (excludedMetaDataIds != null) { + connectorModel.selectAllKeys(); + for (int row = 0; row < connectorModel.getRowCount(); row++) { + Integer metaDataId = (Integer) connectorModel.getValueAt(row, ItemSelectionTableModel.KEY_COLUMN); + if (excludedMetaDataIds.contains(metaDataId)) { + connectorModel.setValueAt(Boolean.FALSE, row, ItemSelectionTableModel.CHECKBOX_COLUMN); + } + } + } + + messageIdLowerField.setText((messageFilter.getMinMessageId() == null) ? "" : String.valueOf(messageFilter.getMinMessageId())); + messageIdUpperField.setText((messageFilter.getMaxMessageId() == null) ? "" : String.valueOf(messageFilter.getMaxMessageId())); + originalIdLowerField.setText((messageFilter.getOriginalIdLower() == null) ? "" : String.valueOf(messageFilter.getOriginalIdLower())); + originalIdUpperField.setText((messageFilter.getOriginalIdUpper() == null) ? "" : String.valueOf(messageFilter.getOriginalIdUpper())); + importIdLowerField.setText((messageFilter.getImportIdLower() == null) ? "" : String.valueOf(messageFilter.getImportIdLower())); + importIdUpperField.setText((messageFilter.getImportIdUpper() == null) ? "" : String.valueOf(messageFilter.getImportIdUpper())); + serverIdField.setText(StringUtils.defaultString(messageFilter.getServerId())); + sendAttemptsLower.setValue((messageFilter.getSendAttemptsLower() == null) ? 0 : messageFilter.getSendAttemptsLower()); + sendAttemptsUpper.setValue((messageFilter.getSendAttemptsUpper() == null) ? "" : String.valueOf(messageFilter.getSendAttemptsUpper())); + attachmentCheckBox.setSelected(Boolean.TRUE.equals(messageFilter.getAttachment())); + errorCheckBox.setSelected(Boolean.TRUE.equals(messageFilter.getError())); + + if (messageFilter.getContentSearch() != null) { + for (ContentSearchElement contentSearchElement : messageFilter.getContentSearch()) { + for (String search : contentSearchElement.getSearches()) { + contentSearchModel.addRow(new Object[] { ContentType.fromCode(contentSearchElement.getContentCode()), search }); + } + } + } + + if (messageFilter.getMetaDataSearch() != null) { + for (MetaDataSearchElement metaDataSearchElement : messageFilter.getMetaDataSearch()) { + if (cachedMetaDataColumns.containsKey(metaDataSearchElement.getColumnName())) { + metaDataSearchModel.addRow(new Object[] { + metaDataSearchElement.getColumnName(), + MetaDataSearchOperator.fromString(metaDataSearchElement.getOperator()), + formatMetaDataValue(metaDataSearchElement.getValue()), + Boolean.TRUE.equals(metaDataSearchElement.getIgnoreCase()) }); + } + } + } + } + + private String formatMetaDataValue(Object value) { + if (value == null) { + return ""; + } + + if (value instanceof java.util.Calendar) { + return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(((java.util.Calendar) value).getTime()); + } + + return String.valueOf(value); + } + private void stopEditing() { // if the user had typed in a value in the content search table, close the cell editor so that any value that was entered will be included in the search TableCellEditor cellEditor = contentSearchTable.getCellEditor(); diff --git a/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowserRecentFilterStore.java b/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowserRecentFilterStore.java new file mode 100644 index 000000000..7a72b036e --- /dev/null +++ b/client/src/com/mirth/connect/client/ui/browsers/message/MessageBrowserRecentFilterStore.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) Mirth Corporation. All rights reserved. + * + * http://www.mirthcorp.com + * + * The software in this package is published under the terms of the MPL license a copy of which has + * been included with this distribution in the LICENSE.txt file. + */ + +package com.mirth.connect.client.ui.browsers.message; + +import java.util.ArrayList; +import java.util.List; +import java.util.prefs.Preferences; + +import org.apache.commons.lang3.StringUtils; + +import com.mirth.connect.client.ui.Mirth; +import com.mirth.connect.model.converters.ObjectXMLSerializer; +import com.mirth.connect.model.filters.MessageFilter; + +class MessageBrowserRecentFilterStore { + private static final int MAX_RECENT_FILTERS = 10; + private static final String RECENT_FILTERS_PREFERENCE_PREFIX = "messageBrowserRecentFilters."; + + private final String prefKey; + + public MessageBrowserRecentFilterStore(String channelId) { + this.prefKey = RECENT_FILTERS_PREFERENCE_PREFIX + channelId; + } + + public List getRecentFilters() { + try { + String serialized = Preferences.userNodeForPackage(Mirth.class).get(prefKey, ""); + if (StringUtils.isBlank(serialized)) return List.of(); + + var result = ObjectXMLSerializer.getInstance().deserialize(serialized, List.class); + if (result == null) return List.of(); + + return (List) result; + } catch (Exception e) { + // Fail quietly if the stored filters cannot be deserialized for any reason. + e.printStackTrace(); + return List.of(); + } + } + + public void addRecentFilter(MessageFilter filter) { + if (filter == null) { + throw new IllegalArgumentException("Filter cannot be null"); + } + + var filters = new ArrayList(getRecentFilters()); + + // Remove then re-add to avoid duplicates + filters.remove(filter); + filters.add(0, filter); + + // Trim to the maximum number of recent filters + if (filters.size() > MAX_RECENT_FILTERS) { + filters.subList(MAX_RECENT_FILTERS, filters.size()).clear(); + } + + try { + var preferences = Preferences.userNodeForPackage(Mirth.class); + preferences.put(prefKey, ObjectXMLSerializer.getInstance().serialize(filters)); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/server/src/com/mirth/connect/model/filters/MessageFilter.java b/server/src/com/mirth/connect/model/filters/MessageFilter.java index 87529fd51..d9a39ed65 100644 --- a/server/src/com/mirth/connect/model/filters/MessageFilter.java +++ b/server/src/com/mirth/connect/model/filters/MessageFilter.java @@ -13,13 +13,21 @@ import java.util.Calendar; import java.util.List; import java.util.Set; +import java.util.Map; +import java.util.Objects; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import com.mirth.connect.donkey.model.message.Status; import com.mirth.connect.model.MessageFilterToStringStyle; import com.mirth.connect.model.filters.elements.ContentSearchElement; import com.mirth.connect.model.filters.elements.MetaDataSearchElement; +import com.mirth.connect.model.filters.elements.MetaDataSearchOperator; +import com.mirth.connect.donkey.model.message.ContentType; + import com.thoughtworks.xstream.annotations.XStreamAlias; /** @@ -28,6 +36,8 @@ */ @XStreamAlias("messageFilter") public class MessageFilter implements Serializable { + private static final MessageFilter EMPTY_FILTER = new MessageFilter(); + /* * Note that any filter criteria that is an int must be represented using Integer otherwise it * will default to 0 and not pass the isNotNull check in the SQL mapping. @@ -51,6 +61,10 @@ public class MessageFilter implements Serializable { private List textSearchMetaDataColumns; private Integer sendAttemptsLower; private Integer sendAttemptsUpper; + /* + * Despite the comment above, "false" for attachment and error actually mean "not set". + * There's no way to search for messages that don't have attachments/errors. + */ private Boolean attachment; private Boolean error; @@ -222,8 +236,244 @@ public void setError(Boolean error) { this.error = error; } + @Override + public boolean equals(Object obj) { + return obj instanceof MessageFilter + && equals((MessageFilter) obj); + } + + /** Check if the filter has logically identical criteria */ + public boolean equals(MessageFilter filter) { + return filter != null + && Objects.equals(maxMessageId, filter.maxMessageId) + && Objects.equals(minMessageId, filter.minMessageId) + && Objects.equals(originalIdUpper, filter.originalIdUpper) + && Objects.equals(originalIdLower, filter.originalIdLower) + && Objects.equals(importIdUpper, filter.importIdUpper) + && Objects.equals(importIdLower, filter.importIdLower) + && Objects.equals(startDate, filter.startDate) + && Objects.equals(endDate, filter.endDate) + && Objects.equals(textSearch, filter.textSearch) + && Objects.equals(textSearchRegex, filter.textSearchRegex) + && Objects.equals(statuses, filter.statuses) + && Objects.equals( + (includedMetaDataIds == null || includedMetaDataIds.isEmpty()) ? null : includedMetaDataIds, + (filter.includedMetaDataIds == null || filter.includedMetaDataIds.isEmpty()) ? null : filter.includedMetaDataIds) + && Objects.equals( + (excludedMetaDataIds == null || excludedMetaDataIds.isEmpty()) ? null : excludedMetaDataIds, + (filter.excludedMetaDataIds == null || filter.excludedMetaDataIds.isEmpty()) ? null : filter.excludedMetaDataIds) + && Objects.equals(serverId, filter.serverId) + && Objects.equals(contentSearch, filter.contentSearch) + && Objects.equals(metaDataSearch, filter.metaDataSearch) + && Objects.equals(textSearchMetaDataColumns, filter.textSearchMetaDataColumns) + && Objects.equals(sendAttemptsLower, filter.sendAttemptsLower) + && Objects.equals(sendAttemptsUpper, filter.sendAttemptsUpper) + && Objects.equals(Boolean.TRUE.equals(attachment), Boolean.TRUE.equals(filter.attachment)) + && Objects.equals(Boolean.TRUE.equals(error), Boolean.TRUE.equals(filter.error)); + } + + @Override + public int hashCode() { + return Objects.hash(maxMessageId, startDate, endDate, textSearch); + } + + /** Check if the filter has any criteria */ + public boolean isEmpty() { + return this.equals(EMPTY_FILTER); + } + + /** Serialize to event log audit string */ @Override public String toString() { return ToStringBuilder.reflectionToString(this, MessageFilterToStringStyle.instance()); } + + /** Serialize to a human-readable version of the filter, shown in MessageBrowser */ + public String toDisplayString() { + return toDisplayString(Map.of(), "\n", false); + } + + /** + * Serialize to a human-readable version of the filter, shown in MessageBrowser + * @param connectors A map of connector IDs to names, used to display connector criteria in a more user-friendly way + * @param padding A string to insert between criteria + * @param includeEmptyCriteria Whether to include criteria that are not set (e.g. "Statuses: (any)") in the output + */ + public String toDisplayString(Map connectors, String padding, boolean includeEmptyCriteria) { + StringBuilder text = new StringBuilder(); + + if (maxMessageId != null) { + text.append("Max Message Id: "); + text.append(maxMessageId); + } + + if (minMessageId != null) { + if (text.length() > 0) text.append(padding); + text.append("Min Message Id: "); + text.append(minMessageId); + } + + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + + if (includeEmptyCriteria || startDate != null || endDate != null) { + if (text.length() > 0) text.append(padding); + text.append("Date Range: "); + + if (startDate == null) { + text.append("(any)"); + } else { + text.append(dateFormat.format(startDate.getTime())); + } + + text.append(" to "); + + if (endDate == null) { + text.append("(any)"); + } else { + text.append(dateFormat.format(endDate.getTime())); + } + } + + if (includeEmptyCriteria || (statuses != null && !statuses.isEmpty())) { + if (text.length() > 0) text.append(padding); + text.append("Statuses: "); + + if (statuses == null) { + text.append("(any)"); + } else { + text.append(StringUtils.join(statuses, ", ")); + } + } + + if (textSearch != null) { + if (text.length() > 0) text.append(padding); + text.append("Text Search: " + textSearch); + } + + if (includeEmptyCriteria + || (includedMetaDataIds != null && !includedMetaDataIds.isEmpty()) + || (excludedMetaDataIds != null && !excludedMetaDataIds.isEmpty())) { + getConnectorSearchCriteriaText(text, padding, connectors); + } + + if (originalIdLower != null || originalIdUpper != null) { + if (text.length() > 0) text.append(padding); + text.append("Original Id: "); + if (originalIdUpper == null) { + text.append("Greater than " + originalIdLower); + } else if (originalIdLower == null) { + text.append("Less than " + originalIdUpper); + } else { + text.append("Between " + originalIdLower + " and " + originalIdUpper); + } + } + + if (importIdLower != null || importIdUpper != null) { + if (text.length() > 0) text.append(padding); + text.append("Import Id: "); + if (importIdUpper == null) { + text.append("Greater than " + importIdLower); + } else if (importIdLower == null) { + text.append("Less than " + importIdUpper); + } else { + text.append("Between " + importIdLower + " and " + importIdUpper); + } + } + + if (serverId != null) { + if (text.length() > 0) text.append(padding); + text.append("Server Id: " + serverId); + } + + if (sendAttemptsLower != null || sendAttemptsUpper != null) { + if (text.length() > 0) text.append(padding); + text.append("# of Send Attempts: "); + + if (sendAttemptsLower != null) { + text.append(sendAttemptsLower); + } else { + text.append("(any)"); + } + + text.append(" - "); + + if (sendAttemptsUpper != null) { + text.append(sendAttemptsUpper); + } else { + text.append("(any)"); + } + } + + if (contentSearch != null) { + for (ContentSearchElement element : contentSearch) { + for (String value : element.getSearches()) { + if (text.length() > 0) text.append(padding); + text.append(ContentType.fromCode(element.getContentCode()) + " contains \"" + value + "\""); + } + } + } + + if (metaDataSearch != null) { + for (MetaDataSearchElement element : metaDataSearch) { + if (text.length() > 0) text.append(padding); + text.append(element.getColumnName() + " " + MetaDataSearchOperator.fromString(element.getOperator()).toString() + " "); + if (element.getValue() instanceof Calendar) { + Calendar date = (Calendar) element.getValue(); + text.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date.getTime())); + } else { + text.append(element.getValue()); + } + if (Boolean.TRUE.equals(element.getIgnoreCase())) { + text.append(" (Ignore Case)"); + } + } + } + + if (Boolean.TRUE.equals(attachment)) { + if (text.length() > 0) text.append(padding); + text.append("Has Attachment"); + } + + if (Boolean.TRUE.equals(error)) { + if (text.length() > 0) text.append(padding); + text.append("Has Error"); + } + + if (text.length() == 0) { + text.append("(no criteria)"); + } + + return text.toString(); + } + + private void getConnectorSearchCriteriaText(StringBuilder text, String padding, Map connectors) { + if (text.length() > 0) text.append(padding); + text.append("Connectors: "); + + if (includedMetaDataIds == null) { + text.append("(any)"); + } else if (includedMetaDataIds.isEmpty()) { + text.append("(none)"); + } else { + boolean first = true; + for (var includedConnectorId : includedMetaDataIds) { + if (!first) text.append(", "); + first = false; + + text.append(connectors.getOrDefault(includedConnectorId, String.valueOf(includedConnectorId))); + } + } + + if (excludedMetaDataIds != null && !excludedMetaDataIds.isEmpty()) { + text.append(" except "); + + boolean first = true; + for (var excludedConnectorId : excludedMetaDataIds) { + if (!first) text.append(", "); + first = false; + + text.append(connectors.getOrDefault(excludedConnectorId, String.valueOf(excludedConnectorId))); + } + } + } } diff --git a/server/src/com/mirth/connect/model/filters/elements/ContentSearchElement.java b/server/src/com/mirth/connect/model/filters/elements/ContentSearchElement.java index fe05d76b6..052b3461b 100644 --- a/server/src/com/mirth/connect/model/filters/elements/ContentSearchElement.java +++ b/server/src/com/mirth/connect/model/filters/elements/ContentSearchElement.java @@ -6,6 +6,7 @@ import java.io.Serializable; import java.util.List; +import java.util.Objects; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -41,4 +42,22 @@ public void setSearches(List searches) { public String toString() { return ToStringBuilder.reflectionToString(this, ContentSearchElementToStringStyle.instance()); } + + @Override + public boolean equals(Object obj) { + return obj instanceof ContentSearchElement + && equals((ContentSearchElement) obj); + } + + /** Check if the element has logically identical criteria */ + public boolean equals(ContentSearchElement element) { + return element != null + && contentCode == element.contentCode + && Objects.equals(searches, element.searches); + } + + @Override + public int hashCode() { + return Objects.hash(contentCode, searches); + } } diff --git a/server/src/com/mirth/connect/model/filters/elements/MetaDataSearchElement.java b/server/src/com/mirth/connect/model/filters/elements/MetaDataSearchElement.java index 9d739d4ca..695c9ca44 100644 --- a/server/src/com/mirth/connect/model/filters/elements/MetaDataSearchElement.java +++ b/server/src/com/mirth/connect/model/filters/elements/MetaDataSearchElement.java @@ -3,6 +3,7 @@ package com.mirth.connect.model.filters.elements; +import java.util.Objects; import java.io.Serializable; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -61,4 +62,24 @@ public void setIgnoreCase(Boolean ignoreCase) { public String toString() { return ToStringBuilder.reflectionToString(this, SearchElementToStringStyle.instance()); } + + @Override + public boolean equals(Object obj) { + return obj instanceof MetaDataSearchElement + && equals((MetaDataSearchElement) obj); + } + + /** Check if the element has logically identical criteria */ + public boolean equals(MetaDataSearchElement element) { + return element != null + && Objects.equals(columnName, element.columnName) + && Objects.equals(operator, element.operator) + && Objects.equals(value, element.value) + && Objects.equals(ignoreCase, element.ignoreCase); + } + + @Override + public int hashCode() { + return Objects.hash(columnName, operator, value, ignoreCase); + } } diff --git a/server/test/com/mirth/connect/model/MessageFilterModelTest.java b/server/test/com/mirth/connect/model/MessageFilterModelTest.java new file mode 100644 index 000000000..7085b1bd0 --- /dev/null +++ b/server/test/com/mirth/connect/model/MessageFilterModelTest.java @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MPL-2.0 + +package com.mirth.connect.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.EnumSet; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import com.mirth.connect.donkey.model.message.ContentType; +import com.mirth.connect.donkey.model.message.Status; +import com.mirth.connect.model.filters.MessageFilter; +import com.mirth.connect.model.filters.elements.ContentSearchElement; +import com.mirth.connect.model.filters.elements.MetaDataSearchElement; + +public class MessageFilterModelTest { + + @Test + public void testMessageFilterEqualityAndToString() { + var left = new MessageFilter(); + var right = new MessageFilter(); + + assertTrue(left.isEmpty()); + assertEquals(left, right); + assertEquals("(no criteria)", left.toDisplayString()); + assertFalse(left.equals((Object) null)); + + left.setMaxMessageId(200L); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setMaxMessageId(200L); + assertEquals(left, right); + + left.setMinMessageId(100L); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setMinMessageId(100L); + assertEquals(left, right); + + left.setOriginalIdLower(10L); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setOriginalIdLower(10L); + assertEquals(left, right); + + left.setOriginalIdUpper(20L); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setOriginalIdUpper(20L); + assertEquals(left, right); + + left.setImportIdLower(25L); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setImportIdLower(25L); + assertEquals(left, right); + + left.setImportIdUpper(30L); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setImportIdUpper(30L); + assertEquals(left, right); + + left.setStartDate(calendar(2024, 1, 2, 3, 4, 0)); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setStartDate(calendar(2024, 1, 2, 3, 4, 0)); + assertEquals(left, right); + + left.setEndDate(calendar(2024, 2, 3, 4, 5, 0)); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setEndDate(calendar(2024, 2, 3, 4, 5, 0)); + assertEquals(left, right); + + left.setTextSearch("alpha"); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setTextSearch("alpha"); + assertEquals(left, right); + + left.setTextSearchRegex(false); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setTextSearchRegex(false); + assertEquals(left, right); + + left.setStatuses(EnumSet.of(Status.RECEIVED, Status.ERROR)); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setStatuses(EnumSet.of(Status.RECEIVED, Status.ERROR)); + assertEquals(left, right); + + left.setIncludedMetaDataIds(List.of(1, 2)); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setIncludedMetaDataIds(List.of(1, 2)); + assertEquals(left, right); + + left.setExcludedMetaDataIds(List.of(3)); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setExcludedMetaDataIds(List.of(3)); + assertEquals(left, right); + + left.setServerId("server-1"); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setServerId("server-1"); + assertEquals(left, right); + + left.setContentSearch(List.of(new ContentSearchElement(ContentType.RAW.getContentTypeCode(), List.of("needle", "haystack")))); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setContentSearch(List.of(new ContentSearchElement(ContentType.RAW.getContentTypeCode(), List.of("needle", "haystack")))); + assertEquals(left, right); + + left.setMetaDataSearch(List.of(new MetaDataSearchElement("mirth_type", "EQUAL", "asdf", null))); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setMetaDataSearch(List.of(new MetaDataSearchElement("mirth_type", "EQUAL", "asdf", null))); + assertEquals(left, right); + + left.setTextSearchMetaDataColumns(List.of("columnA", "columnB")); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setTextSearchMetaDataColumns(List.of("columnA", "columnB")); + assertEquals(left, right); + + left.setSendAttemptsLower(1); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setSendAttemptsLower(1); + assertEquals(left, right); + + left.setSendAttemptsUpper(5); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setSendAttemptsUpper(5); + assertEquals(left, right); + + left.setAttachment(true); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setAttachment(true); + assertEquals(left, right); + + left.setError(true); + assertNotEquals(left, right); + assertNotNull(left.toDisplayString()); + right.setError(true); + assertEquals(left, right); + + var expected = String.join("\n", + "Max Message Id: 200", + "Min Message Id: 100", + "Date Range: 2024-01-02 03:04 to 2024-02-03 04:05", + "Statuses: RECEIVED, ERROR", + "Text Search: alpha", + "Connectors: Source, Destination except Filtered", + "Original Id: Between 10 and 20", + "Import Id: Between 25 and 30", + "Server Id: server-1", + "# of Send Attempts: 1 - 5", + "Raw contains \"needle\"", + "Raw contains \"haystack\"", + "mirth_type = asdf", + "Has Attachment", + "Has Error" + ); + + assertEquals(expected, left.toDisplayString(Map.of(1, "Source", 2, "Destination", 3, "Filtered"), "\n", false)); + } + + @Test + public void testMessageFilterToStringFallsBackToConnectorIds() { + var filter = new MessageFilter(); + filter.setIncludedMetaDataIds(List.of(1, 99)); + filter.setExcludedMetaDataIds(List.of(3, 42)); + + assertEquals( + "Connectors: Source, 99 except Filtered, 42", + filter.toDisplayString(Map.of(1, "Source", 3, "Filtered"), "\n", false) + ); + } + + @Test + public void testMessageFilterToStringIncludesEmptyCriteria() { + var filter = new MessageFilter(); + filter.setMaxMessageId(1L); + + assertEquals( + String.join("\n", + "Max Message Id: 1", + "Date Range: (any) to (any)", + "Statuses: (any)", + "Connectors: (any)" + ), + filter.toDisplayString(Map.of(), "\n", true) + ); + } + + private GregorianCalendar calendar(int year, int month, int day, int hour, int minute, int second) { + GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day, hour, minute, second); + calendar.set(GregorianCalendar.MILLISECOND, 0); + return calendar; + } +} \ No newline at end of file