Skip to content

Conversation

@rogin
Copy link
Contributor

@rogin rogin commented Apr 19, 2025

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.

@tonygermano
Copy link
Member

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.

jonbartels
jonbartels previously approved these changes Apr 20, 2025
kayyagari
kayyagari previously approved these changes Aug 18, 2025
@NicoPiel
Copy link
Collaborator

NicoPiel commented Sep 3, 2025

Can this be revisited now? @tonygermano

gibson9583
gibson9583 previously approved these changes Dec 10, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • DestinationSet now implements Set<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:

  1. NullPointerException bug: The constructor leaves metaDataIds null when the source map doesn't contain the required key, causing NPEs in all Set methods
  2. Test reliability: Multiple tests assume HashSet iteration order, which is not guaranteed and could cause intermittent test failures
  3. 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.


@Override
public boolean retainAll(Collection<?> metaDataIdOrConnectorNames) {
return removeAllExcept((Collection<Object>)metaDataIdOrConnectorNames);
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchecked cast from Collection<?> to Collection<Object>. While this works, it will generate a compiler warning. Consider adding @SuppressWarnings("unchecked") to this method or refactoring to avoid the cast.

Copilot uses AI. Check for mistakes.

@Override
public boolean removeAll(Collection<?> metaDataIdOrConnectorNames) {
return remove((Collection<Object>)metaDataIdOrConnectorNames);
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchecked cast from Collection<?> to Collection<Object>. While this works, it will generate a compiler warning. Consider adding @SuppressWarnings("unchecked") to this method or refactoring to avoid the cast.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +65
@Test
public void testIsEmptyWithoutSourceMapKey() throws Exception {
DestinationSet ds = getNewTestDestinationSetUsing(new Integer[0]);

assertTrue(ds.isEmpty());
assertEquals(0, ds.size());
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name testIsEmptyWithoutSourceMapKey is misleading. The test actually creates a source map WITH the key (line 47 in getNewTestDestinationSetUsing), just with an empty set.

There's no test coverage for the scenario where the source map does NOT contain Constants.DESTINATION_SET_KEY at all (i.e., when line 45 in DestinationSet.java returns false). This is a critical gap because that scenario will cause a NullPointerException with the current implementation.

Add a test like:

@Test
public void testWithoutSourceMapKey() throws Exception {
    ConnectorMessage cm = new ConnectorMessage();
    Map<String, Object> sm = new HashMap<>();
    // Don't put DESTINATION_SET_KEY in the map
    cm.setSourceMap(sm);
    ImmutableConnectorMessage icm = new ImmutableConnectorMessage(cm);
    DestinationSet ds = new DestinationSet(icm);
    
    assertTrue(ds.isEmpty());
    assertEquals(0, ds.size());
}

Copilot uses AI. Check for mistakes.
Comment on lines +353 to +366
public void testToArrayGenericProperlySized() throws Exception {
DestinationSet ds = getNewTestDestinationSet();

Integer[] arr = new Integer[ds.size()];
//the input and response should be the same array
Integer[] resp = ds.toArray(arr);
assertEquals(3, resp.length);
assertTrue(Arrays.equals(arr, resp));
assertEquals(Integer.valueOf(1), arr[0]);
assertEquals(Integer.valueOf(2), arr[1]);
assertEquals(Integer.valueOf(3), arr[2]);
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test assumes a specific array order (1, 2, 3), but DestinationSet uses a HashSet internally, which does not guarantee any specific order. The test is flawed and could fail intermittently.

The test should verify that all expected elements are present without assuming order.

Copilot uses AI. Check for mistakes.
DestinationSet ds = getNewTestDestinationSet();

Integer[] arr = new Integer[ds.size()];
//the input and response should be the same array
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment formatting should follow convention with a space after //. Should be // the input and response should be the same array.

Suggested change
//the input and response should be the same array
// the input and response should be the same array

Copilot uses AI. Check for mistakes.

assertFalse(ds.contains(0));
assertTrue(ds.contains(1));
assertTrue(ds.contains("Map to 2"));
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actual argument type 'String' is incompatible with expected argument type 'Integer'.

Suggested change
assertTrue(ds.contains("Map to 2"));
assertTrue(ds.contains(2));

Copilot uses AI. Check for mistakes.
assertTrue(ds.contains("Map to 2"));
assertTrue(ds.contains(3));
assertFalse(ds.contains(4));
assertFalse(ds.contains("Invalid"));
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actual argument type 'String' is incompatible with expected argument type 'Integer'.

Copilot uses AI. Check for mistakes.
* @return A boolean indicating whether at least one destination connector was actually removed
* from processing for this message.
*/
public boolean removeAllExcept(Object metaDataIdOrConnectorName) {
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method DestinationSet.removeAllExcept(..) could be confused with overloaded method removeAllExcept, since dispatch depends on static types.

Copilot uses AI. Check for mistakes.
* @return A boolean indicating whether at least one destination connector was actually removed
* from processing for this message.
*/
public boolean remove(Object metaDataIdOrConnectorName) {
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method DestinationSet.remove(..) could be confused with overloaded method remove, since dispatch depends on static types.

Copilot uses AI. Check for mistakes.
* destination connector name.
* @return A boolean indicating whether at least one destination connector was actually removed
* from processing for this message.
*/
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method overrides Set.remove; it is advisable to add an Override annotation.

Suggested change
*/
*/
@Override

Copilot uses AI. Check for mistakes.
Comment on lines +342 to +352
public void testToArray() throws Exception {
DestinationSet ds = getNewTestDestinationSet();

Object[] arr = ds.toArray();
assertEquals(3, arr.length);
assertEquals(Integer.valueOf(1), arr[0]);
assertEquals(Integer.valueOf(2), arr[1]);
assertEquals(Integer.valueOf(3), arr[2]);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true. Tests that assume toArray to be deterministic could be flaky.

Comment on lines 44 to 50
try {
if (connectorMessage.getSourceMap().containsKey(Constants.DESTINATION_SET_KEY)) {
this.destinationIdMap = connectorMessage.getDestinationIdMap();
this.metaDataIds = (Set<Integer>) connectorMessage.getSourceMap().get(Constants.DESTINATION_SET_KEY);
}
} catch (Exception e) {
metaDataIds = new HashSet<>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a good point and probable cause for future NPEs.

Copy link
Contributor

@pacmano1 pacmano1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@rogin
Copy link
Contributor Author

rogin commented Dec 17, 2025

I'm impressed by the copilot results. I'd like to resolve the findings this weekend.

@david-labs-ca
Copy link

Impressive work.

Signed-off-by: Richard Ogin <rogin@users.noreply.github.com>
Copy link
Member

@tonygermano tonygermano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}

return false;
return remove(Collections.singleton(metaDataIdOrConnectorName));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: annotate with @Override

This is a Set operation


suggestion: keep this method simple and don't wrap and delegate

If metaDataIds is made final and never null above, then the first null check can be removed.

}

return removed;
if(metaDataIdOrConnectorNames == null) { return false; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: follow copilot recommendation to add space in if (


suggestion: move this implementation to Set operation removeAll and deprecate this method

This encourages the use of the Set operations remove for a single object and removeAll for collections. The overloaded remove has been a source of confusion in the past.


issue: should throw a NPE instead of return false when Collection is null

This matches the previous behavior and is specified by Set.removeAll

Comment on lines +78 to +79
.filter(Boolean::booleanValue)
.count() > 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.filter(Boolean::booleanValue)
.count() > 0;
.anyMatch(Boolean::booleanValue)

}

return false;
return removeAllExcept(Collections.singleton(metaDataIdOrConnectorName));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: make this a call to Set operation retainAll

}

return false;
if(metaDataIdOrConnectorNames == null) { return false; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: follow copilot recommendation to add space in if (


suggestion: move this implementation to Set operation retainAll and deprecate this method

This encourages the use of the Set operations and reduces confusion about two methods which do the same thing. I think removeAllExcept(Object) can remain since Set does not provide a retainOnly method for a single Object.


issue: should throw a NPE instead of return false when Collection is null

This matches the previous behavior and is specified by Set.retainAll


Suggestion: remove null check for metaDataIds

It should not be null if following other recommendations in this review

}

@Override
public boolean addAll(Collection<? extends Integer> metaDataIdOrConnectorNames) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: addAll should throw UnsupportedOperationException

DestinationSets do not support adding to the Set

}

@Override
public boolean retainAll(Collection<?> metaDataIdOrConnectorNames) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: deprecate removeAllExcept(Collection<Object>) in favor of this method

Also mentioned above. The Set operations should be the primary methods to reduce confusion and we can eventually remove the old methods.

}

@Override
public boolean removeAll(Collection<?> metaDataIdOrConnectorNames) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: deprecate remove(Collection<Object>) in favor of this method

Also mentioned above. The Set operations should be the primary methods to reduce confusion and we can eventually remove the old methods.

Comment on lines +157 to +159
Optional<Integer> m = convertToMetaDataId(metaDataIdOrConnectorName);

return m.isPresent() && metaDataIds.contains(m.get());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if convertToMetaDataId is changed back to returning nulls and metaDataIds is always non-null as suggested elsewhere, I think this can be simplified to just

return metaDataIds.contains(convertToMetaDataId(metaDataIdOrConnectorName));


@Override
public boolean containsAll(Collection<?> metaDataIdOrConnectorNames) {
if(metaDataIdOrConnectorNames == null) { return false; }
Copy link
Member

@tonygermano tonygermano Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: follow copilot recommendation to add space in if (


issue: Set.containsAll should throw a NPE instead of returning false when Collection is null

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants