Repository navigation
DestinationSet should implement java.util.Set - #44
Conversation
|
Thanks for submitting this! I always meant to give it a more thorough review and suggest a few things. Just a heads up, this probably will not be a top priority until after we get our first release out the door. |
|
Can this be revisited now? @tonygermano |
There was a problem hiding this comment.
Pull request overview
This PR implements the java.util.Set<Integer> interface for the DestinationSet class, addressing a long-standing feature request. The implementation adds standard Set methods (add, contains, iterator, etc.) and modernizes the code with Java 8 streams and Optional.
Key Changes:
DestinationSetnow implementsSet<Integer>, enabling use as a standard Java collection- Refactored existing methods to use streams and Optional for cleaner code
- Added comprehensive test suite with 20+ test cases covering Set interface methods
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 21 comments.
| File | Description |
|---|---|
| server/src/com/mirth/connect/server/userutil/DestinationSet.java | Implements Set interface with all required methods, refactors internal logic to use streams and Optional |
| server/test/com/mirth/connect/userutil/DestinationSetTest.java | Adds comprehensive test suite for Set interface implementation and existing functionality |
Critical Issues Found:
- NullPointerException bug: The constructor leaves
metaDataIdsnull when the source map doesn't contain the required key, causing NPEs in all Set methods - Test reliability: Multiple tests assume HashSet iteration order, which is not guaranteed and could cause intermittent test failures
- Missing test coverage: No test for the scenario where the source map lacks the
DESTINATION_SET_KEY
Code Quality Issues:
- Inconsistent spacing in control flow statements (
if,for) - Unchecked casts that will generate compiler warnings
- Comment formatting inconsistencies
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pacmano1
left a comment
There was a problem hiding this comment.
Needs more review.
"add" should be in it own PR. We should try to avoid adding features in the context of a limited scope issue.
|
I'm impressed by the copilot results. I'd like to resolve the findings this weekend. |
|
Impressive work. |
3e60899
2b23e77 to
3e60899
Compare
tonygermano
left a comment
There was a problem hiding this comment.
In addition to the comments below, I pushed a change to your branch with the following:
- I fixed the line endings on DestinationSet so that you were no longer rewriting the entire file.
- I changed the header on DestinationSet and added one to DestinationSetTest in SPDX format and adding your name to the copyright.
- I rebased to resolve the merge conflict with previous PR #133 (keeps the changes in this PR, which negate the changes from the previous PR.)
The previous PR also added tests, but to file server/test/com/mirth/connect/server/userutil/DestinationSetTest.java instead of server/test/com/mirth/connect/userutil/DestinationSetTest.java. I think the other PR was correct, and this PR should add to the other tests. I think this is also what is currently causing the build to fail.
|
@tonygermano it doesn't look like there are any objections to your comments/improvements above. Would you be able to make the changes and rebase? We should then be able to get this merged. |
Cover cases that DestinationSet must keep when it is made to implement java.util.Set: - Metadata ids still work when the message has no destination id map, and connector names are not matched. - remove(Collection) and removeAllExcept(Collection) throw a NullPointerException for a null collection. - Calls from JavaScript resolve to the expected overloads for numbers, connector names, and arrays. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Tony Germano <tony@germano.name>
tonygermano
left a comment
There was a problem hiding this comment.
@rogin I rebased this onto main and made the changes from my review, so it should be ready to merge. Since main moved to the Gradle layout, I rebuilt the branch as three commits instead of rebasing the old ones. You are still the author of the main commit.
- Add more DestinationSet tests for the current behavior. These tests pass on the old code and pin down what must not change: metadata ids still work when there is no destination id map, a null collection throws an NPE, and JavaScript calls (numbers, names, and arrays) resolve to the expected overloads.
- Make DestinationSet implement java.util.Set. Your change, with the review comments addressed:
- It extends
AbstractSet<Integer>, soequals,hashCode, andtoStringfollow the Set contract. - Both fields are final and never null. A missing destination set or destination id map falls back to an empty collection. The old branch threw an NPE in both cases.
remove(Object)has@Overrideand no longer wraps and delegates.convertToMetaDataIdreturns null again instead of anOptional, and has Javadoc.removeAll(Collection)andretainAll(Collection)hold the implementations.remove(Collection)andremoveAllExcept(Collection)delegate to them, andremoveAllExcept(Object)callsretainAll.- I changed my mind about deprecating
remove(Collection)andremoveAllExcept(Collection). They stay as supported aliases. If they were ever removed, Rhino would send a JavaScript array to theObjectoverloads instead, and the array doesn't match any destination.destinationSet.remove([...])would silently remove nothing, anddestinationSet.removeAllExcept([...])would silently remove every destination. - A null collection throws an NPE in
removeAll,retainAll, andcontainsAll. addandaddAllthrowUnsupportedOperationException, as @pacmano1 asked.- The iterator is modifiable, so
iterator().remove()andremoveIfwork. removeAll(Collection)uses a loop rather than.anyMatch(...).anyMatchstops at the first match, so it would only remove the first destination that matched.- Your tests are merged into the existing
DestinationSetTest, without the assumptions about iteration order.
- It extends
- Generate destinationSet calls with the Set operations. The Destination Set Filter step and the reference list now generate
removeAll([...])andretainAll([...])instead of the aliases, so new scripts use the Set operations.
A full ./gradlew clean build passes at each commit.
Drafted with Claude Code
|
@tonygermano, can this be tested in CI? |
@mgaffigan is that a question or a request? 🙂 It probably can be, but the unit tests are pretty thorough and test calling from javascript. |
It's a question. I'm not sure how this is mean to be used in reality and am thus unwilling to approve - but I'm assuming it is from some source transformer or from the destination selector step. Unit tests are great - unit tests plus real-world exercise is better. For example: without spelunking, it's hard for me to know if something subclasses DestinationSet between here and message processing that would break real-world, but allow unit tests to pass. |
@mgaffigan it's a class that is part of the User API. The engine presents an instance to the user in the javascript context for modification, but never accepts one, so there isn't really a chance for a subclass to break something. |
|
@mgaffigan these changes are meant to make it behave more like a normal Set, since the original class did not allow inspection or iteration. It retains the original property of the class that additions are not allowed. I think any potential issue that I'm not aware of with subclassing would be an existing issue before these changes. |
pacmano1
left a comment
There was a problem hiding this comment.
Ran it on a real engine next to main, same channels, same messages: existing destinationSet calls return and route exactly the same in the source filter and transformer, both preprocessors and the Destination Set Filter step, including partial destination lists, router.routeMessageByChannelId and reprocess. @mgaffigan, that's the real-world run.
Release notes: logging it changes. String(destinationSet) was ...DestinationSet@2efe6d42, now [1, 2, 3, 4].
Worth a line of Javadoc, not blocking:
iterator().next()andtoArray()givejava.lang.Integerobjects, soit.next() === 2is false. TheforEach/removeIfcallbacks get JS numbers.destinationSet.remove(id)insideforEachor an iterator loop throwsConcurrentModificationException;removeIforiterator().remove()is the way.for each (var id in destinationSet)loops over the methods, andfor (var id of destinationSet)says it isn't iterable (same on main).
Co-authored with Claude Code.
One channel with three destinations, six messages: the Destination Set Filter step's generated removeAll([...]) and retainAll([...]), the existing remove([...]) and removeAllExcept(...) calls, and the new size()/contains() and removeIf(). The transformer runs each message that starts with destinationSet as the call under test and writes what it returned and the destinations left into the transformed message, so source_transformed also covers destinations that were removed; the harness can only assert the ones that were reached. Against main's server the first four pass and size()/contains() and removeIf() fail with "Cannot find function". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com>
|
Pushed a CI fixture, On the Co-authored with Claude Code. |
This behavior is from the iterator of
I believe the |
|
@NicoPiel, are your requested changes complete? If we don't hear back by EOD Friday, I'm going to dismiss them since this is otherwise ready to merge. |
|
@mgaffigan My conversations seem to be resolved? I don't know what else to dismiss. |
|
@NicoPiel, it shows you're no longer requesting changes now. Thanks. To unblock without approval, you use the "Dismiss" under your name at the top right - otherwise requesting changes blocks until approval whether conversations are open or not. |
DestinationSet now extends AbstractSet<Integer>, so scripts can treat destinationSet as a set of the metaDataIds the message will be sent to. The methods that look up or remove destinations still accept either a metaDataId or a destination connector name. - remove(Object), removeAll(Collection), retainAll(Collection), and contains(Object) are the Set operations. remove(Collection) and removeAllExcept(Collection) stay as aliases of removeAll and retainAll, and removeAllExcept(Object) now delegates to retainAll. - removeAll(Collection), retainAll(Collection), and containsAll throw a NullPointerException for a null collection, as Set requires and as the old methods did. - add and addAll throw UnsupportedOperationException. Destinations can only be removed. - The iterator supports remove, so removeIf also works. - equals, hashCode, and toString follow the Set contract. - When the source map has no destination set, or the message has no destination id map, the fields fall back to empty collections instead of null. Both fields are now final. remove(Collection) and removeAllExcept(Collection) are not deprecated. Without them, Rhino would pass a JavaScript array to remove(Object) or removeAllExcept(Object). The array would not match any destination, so destinationSet.remove([...]) would silently remove nothing and destinationSet.removeAllExcept([...]) would silently remove every destination. This reworks the original commit from #44, 3e60899, to address review comments and to move it to the Gradle source layout. Co-authored-by: Tony Germano <tony@germano.name> Issue: nextgenhealthcare/connect#4414 Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Richard Ogin <rogin@users.noreply.github.com> Signed-off-by: Tony Germano <tony@germano.name>
This was an old feature, requested by several people, that NextGen chose to ignore. Prior code review comments are here. I compiled it with Java 8, and the test ran successfully. No further testing on my part.
Release Note
destinationSetis now ajava.util.SetThe
destinationSetobject in the preprocessor and source filter/transformer is now ajava.util.Set<Integer>of the metadata IDs of the destinations the message will be sent to. Scripts can use the standard Set methods, such assize(),isEmpty(),contains(...),removeAll([...]),retainAll([...]),removeIf(...),iterator()andforEach(...). As with the existing methods, the methods that look up or remove destinations accept a metadata ID or a destination connector name:As before, destinations can't be added.
addandaddAllthrowUnsupportedOperationException.Existing scripts don't need any changes.
Upgrade considerations:
destinationSetto a string, or logging it, now gives the remaining metadata IDs, for example[1, 3], instead of a class name and hash such ascom.mirth.connect.server.userutil.DestinationSet@1b2c3d.