diff --git a/api/src/main/java/org/apache/cloudstack/context/ResponseMessageResolver.java b/api/src/main/java/org/apache/cloudstack/context/ResponseMessageResolver.java index 672fc29df97..103dbf58656 100644 --- a/api/src/main/java/org/apache/cloudstack/context/ResponseMessageResolver.java +++ b/api/src/main/java/org/apache/cloudstack/context/ResponseMessageResolver.java @@ -129,12 +129,14 @@ public class ResponseMessageResolver { if (MapUtils.isNotEmpty(metadata)) { for (Map.Entry entry : metadata.entrySet()) { Object value = entry.getValue(); - stringMap.put(entry.getKey(), getMetadataObjectStringValue(value)); + stringMap.put(entry.getKey(), getMetadataObjectStringValueAlt(value)); } } return stringMap; } + + /** * Converts a metadata object to a human-readable string for error messages. * @@ -212,6 +214,39 @@ public class ResponseMessageResolver { return sb.toString(); } + /** + * Converts a metadata object to a human-readable string, prioritizing toString(). + * + *

Behavior: + *

+ * + * @param obj metadata object + * @return formatted metadata string suitable for inclusion in error messages, or {@code null} + * if {@code obj} is {@code null} + */ + protected static String getMetadataObjectStringValueAlt(Object obj) { + if (obj == null) { + return null; + } + String result = obj.toString(); + if (StringUtils.isNotEmpty(result)) { + // Remove id: DBID pattern for non-root admins + if (!CallContext.current().isCallingAccountRootAdmin()) { + result = result.replaceAll("\\bid:\\s*\\d+", "").trim(); + } + if (StringUtils.isNotEmpty(result)) { + return result; + } + } + return getMetadataObjectStringValue(obj); + } + protected static String invokeStringGetter(Object obj, String methodName) { try { Class cls = obj.getClass(); diff --git a/api/src/test/java/org/apache/cloudstack/context/ResponseMessageResolverTest.java b/api/src/test/java/org/apache/cloudstack/context/ResponseMessageResolverTest.java index 2089f544292..545b34350b0 100644 --- a/api/src/test/java/org/apache/cloudstack/context/ResponseMessageResolverTest.java +++ b/api/src/test/java/org/apache/cloudstack/context/ResponseMessageResolverTest.java @@ -441,6 +441,73 @@ public class ResponseMessageResolverTest { Assert.assertEquals(Map.of("field", "value"), response.getErrorMetadata()); } + @Test + public void getMetadataObjectStringValueAlt_shouldReturnNullWhenObjectIsNull() { + Assert.assertNull(ResponseMessageResolver.getMetadataObjectStringValueAlt(null)); + } + + @Test + public void getMetadataObjectStringValueAlt_shouldReturnToStringWhenNonEmptyForRootAdmin() { + when(callContextMock.isCallingAccountRootAdmin()).thenReturn(true); + Object obj = new Object() { + @Override public String toString() { return "SomeObject id: 42"; } + }; + Assert.assertEquals("SomeObject id: 42", ResponseMessageResolver.getMetadataObjectStringValueAlt(obj)); + } + + @Test + public void getMetadataObjectStringValueAlt_shouldStripIdPatternForNonRootAdmin() { + when(callContextMock.isCallingAccountRootAdmin()).thenReturn(false); + Object obj = new Object() { + @Override public String toString() { return "SomeObject id: 42"; } + }; + Assert.assertEquals("SomeObject", ResponseMessageResolver.getMetadataObjectStringValueAlt(obj)); + } + + @Test + public void getMetadataObjectStringValueAlt_shouldStripMultipleIdPatternsForNonRootAdmin() { + when(callContextMock.isCallingAccountRootAdmin()).thenReturn(false); + Object obj = new Object() { + @Override public String toString() { return "id: 1 SomeObject id: 42"; } + }; + Assert.assertEquals("SomeObject", ResponseMessageResolver.getMetadataObjectStringValueAlt(obj)); + } + + @Test + public void getMetadataObjectStringValueAlt_shouldReturnToStringUnchangedWhenNoIdPatternForNonRootAdmin() { + when(callContextMock.isCallingAccountRootAdmin()).thenReturn(false); + Object obj = new Object() { + @Override public String toString() { return "SomeObjectWithoutId"; } + }; + Assert.assertEquals("SomeObjectWithoutId", ResponseMessageResolver.getMetadataObjectStringValueAlt(obj)); + } + + @Test + public void getMetadataObjectStringValueAlt_shouldFallbackToGetMetadataObjectStringValueWhenToStringIsEmpty() { + Identity identityMock = Mockito.mock(Identity.class); + when(identityMock.toString()).thenReturn(""); + when(identityMock.getUuid()).thenReturn("uuid-fallback"); + Assert.assertEquals("uuid-fallback", ResponseMessageResolver.getMetadataObjectStringValueAlt(identityMock)); + } + + @Test + public void getMetadataObjectStringValueAlt_shouldFallbackWhenToStringBecomesBlankAfterStrippingIdForNonRootAdmin() { + when(callContextMock.isCallingAccountRootAdmin()).thenReturn(false); + DataCenter dataCenterMock = Mockito.mock(DataCenter.class); + when(dataCenterMock.toString()).thenReturn("id: 99"); + when(dataCenterMock.getName()).thenReturn("FallbackName"); + Assert.assertEquals("'FallbackName'", ResponseMessageResolver.getMetadataObjectStringValueAlt(dataCenterMock)); + } + + @Test + public void getMetadataObjectStringValueAlt_shouldPreserveIdInToStringForRootAdmin() { + when(callContextMock.isCallingAccountRootAdmin()).thenReturn(true); + Object obj = new Object() { + @Override public String toString() { return "Zone [id: 7, name: TestZone]"; } + }; + Assert.assertEquals("Zone [id: 7, name: TestZone]", ResponseMessageResolver.getMetadataObjectStringValueAlt(obj)); + } + @Test public void updateExceptionResponse_shouldHandleNullMetadata() { ExceptionResponse response = new ExceptionResponse();