2222import java .lang .reflect .InvocationTargetException ;
2323import java .nio .file .Files ;
2424import java .nio .file .Paths ;
25+ import java .util .ArrayList ;
2526import java .util .Collections ;
2627import java .util .LinkedHashMap ;
28+ import java .util .List ;
2729import java .util .Map ;
30+ import java .util .regex .Matcher ;
31+ import java .util .regex .Pattern ;
2832
2933import org .apache .cloudstack .api .Identity ;
3034import org .apache .cloudstack .api .InternalIdentity ;
4145import com .fasterxml .jackson .core .type .TypeReference ;
4246import com .fasterxml .jackson .databind .ObjectMapper ;
4347
44- public final class ErrorMessageResolver {
45-
48+ public class ErrorMessageResolver {
4649 private static final Logger LOG =
4750 LogManager .getLogger (ErrorMessageResolver .class );
4851
49- private static final String ERROR_MESSAGES_FILENAME = "error-messages.json" ;
50- private static final String ERROR_KEY_ADMIN_SUFFIX = ".admin" ;
51- private static final boolean INCLUDE_METADATA_ID_IN_MESSAGE = false ;
52+ protected static final String ERROR_MESSAGES_FILENAME = "error-messages.json" ;
53+ protected static final String ERROR_KEY_ADMIN_SUFFIX = ".admin" ;
54+ protected static final boolean INCLUDE_METADATA_ID_IN_MESSAGE = false ;
55+
56+ private static final Pattern VARIABLE_PATTERN = Pattern .compile ("\\ {\\ {\\ s*([A-Za-z0-9_]+)\\ s*\\ }\\ }" );
5257
5358 private static final ObjectMapper MAPPER = new ObjectMapper ();
5459
@@ -61,11 +66,43 @@ public final class ErrorMessageResolver {
6166 private ErrorMessageResolver () {
6267 }
6368
64- public static String getMessage (String errorKey , Map <String , Object > metadata ) {
65- return getMessageUsingStringMap (errorKey , getStringMap (metadata ));
69+ protected static List <String > getVariableNamesInErrorKey (String template ) {
70+ if (template == null || template .isEmpty ()) {
71+ return Collections .emptyList ();
72+ }
73+ List <String > variables = new ArrayList <>();
74+ Matcher matcher = VARIABLE_PATTERN .matcher (template );
75+ while (matcher .find ()) {
76+ String name = matcher .group (1 );
77+ if (name != null && !name .isEmpty ()) {
78+ variables .add (name );
79+ }
80+ }
81+ return variables ;
82+ }
83+
84+ protected static Map <String , Object > getCombinedMetadataFromErrorTemplate (String template , Map <String , Object > metadata ) {
85+ List <String > variableNames = getVariableNamesInErrorKey (template );
86+ if (variableNames .isEmpty ()) {
87+ return metadata ;
88+ }
89+ Map <String , Object > contextMetadata = CallContext .current ().getContextStringKeyParameters ();
90+ if (MapUtils .isEmpty (contextMetadata )) {
91+ return metadata ;
92+ }
93+ Map <String , Object > combinedMetadata = new LinkedHashMap <>();
94+ for (String varName : variableNames ) {
95+ if (contextMetadata .containsKey (varName )) {
96+ combinedMetadata .put (varName , contextMetadata .get (varName ));
97+ }
98+ }
99+ if (MapUtils .isNotEmpty (metadata )) {
100+ combinedMetadata .putAll (metadata );
101+ }
102+ return combinedMetadata ;
66103 }
67104
68- private static String getTemplateForKey (String errorKey ) {
105+ protected static String getTemplateForKey (String errorKey ) {
69106 if (errorKey == null ) {
70107 return null ;
71108 }
@@ -79,15 +116,7 @@ private static String getTemplateForKey(String errorKey) {
79116 return templates .get (errorKey );
80117 }
81118
82- private static String getMessageUsingStringMap (String errorKey , Map <String , String > metadata ) {
83- String template = getTemplateForKey (errorKey );
84- if (template == null ) {
85- return errorKey ;
86- }
87- return expand (template , metadata );
88- }
89-
90- private static Map <String , String > getStringMap (Map <String , Object > metadata ) {
119+ protected static Map <String , String > getStringMap (Map <String , Object > metadata ) {
91120 Map <String , String > stringMap = new LinkedHashMap <>();
92121 if (MapUtils .isNotEmpty (metadata )) {
93122 for (Map .Entry <String , Object > entry : metadata .entrySet ()) {
@@ -98,16 +127,35 @@ private static Map<String, String> getStringMap(Map<String, Object> metadata) {
98127 return stringMap ;
99128 }
100129
101- private static String getMetadataObjectStringValue (Object obj ) {
130+ /**
131+ * Converts a metadata object to a human-readable string for error messages.
132+ *
133+ * <p>Behavior:
134+ * <ul>
135+ * <li>If {@code obj} is {@code null}, returns {@code null}.</li>
136+ * <li>Attempts to obtain a display name by invoking one of the getters
137+ * {@code getDisplayText()}, {@code getDisplayName()}, or {@code getName()} via reflection.
138+ * If a name is found, returns it quoted as {@code 'NAME'}.</li>
139+ * <li>When the current calling account is a root admin, the returned value will include
140+ * an identifier suffix in the form {@code (ID: id, UUID: uuid)} when available.
141+ * The ID is included only if {@code INCLUDE_METADATA_ID_IN_MESSAGE} is {@code true}
142+ * and {@code obj} implements {@link InternalIdentity}. The UUID is included when
143+ * {@code obj} implements {@link org.apache.cloudstack.api.Identity}.</li>
144+ * <li>If no display name is available, returns the UUID (if {@code obj} implements
145+ * {@code Identity}); otherwise returns {@code obj.toString()}.</li>
146+ * </ul>
147+ *
148+ * <p>Reflection is used to call getters; invocation failures are silently ignored and treated as
149+ * absence of the corresponding value.
150+ *
151+ * @param obj metadata object
152+ * @return formatted metadata string suitable for inclusion in error messages, or {@code null}
153+ * if {@code obj} is {@code null}
154+ */
155+ protected static String getMetadataObjectStringValue (Object obj ) {
102156 if (obj == null ) {
103157 return null ;
104158 }
105- // obj is of primitive type
106- // String value structure should be 'NAME' (ID: id, UUID: uuid)
107- // NAME is obtained from obj.getName() or obj.getDisplayText()
108- // ID is obtained from obj.getId() if obj instanceof InternalIdentity and only for root admin
109- // UUID is obtained from obj.getUuid() if obj instanceof Identity
110- // If NAME is not available, fallback to obj.toString() then simply return UUID if available
111159 String uuid = null ;
112160 if (obj instanceof Identity ) {
113161 uuid = ((Identity ) obj ).getUuid ();
@@ -156,7 +204,7 @@ private static String getMetadataObjectStringValue(Object obj) {
156204 return sb .toString ();
157205 }
158206
159- private static String invokeStringGetter (Object obj , String methodName ) {
207+ protected static String invokeStringGetter (Object obj , String methodName ) {
160208 try {
161209 Class <?> cls = obj .getClass ();
162210 var m = cls .getMethod (methodName );
@@ -167,28 +215,7 @@ private static String invokeStringGetter(Object obj, String methodName) {
167215 }
168216 }
169217
170- public static void updateExceptionResponse (ExceptionResponse response , CloudRuntimeException cre ) {
171- String key = cre .getMessageKey ();
172- Map <String , Object > map = cre .getMetadata ();
173-
174- if (key == null ) {
175- if (cre .getCause () instanceof InvalidParameterValueException ) {
176- key = ((InvalidParameterValueException ) cre .getCause ()).getMessageKey ();
177- map = ((InvalidParameterValueException ) cre .getCause ()).getMetadata ();
178- } else {
179- return ;
180- }
181- }
182- response .setErrorTextKey (key );
183- Map <String , String > stringMap = getStringMap (map );
184- String message = getMessageUsingStringMap (key , stringMap );
185- if (message != null ) {
186- response .setErrorText (message );
187- }
188- response .setErrorMetadata (stringMap );
189- }
190-
191- private static synchronized void reloadIfRequired () {
218+ protected static synchronized void reloadIfRequired () {
192219 try {
193220 // log current directory for debugging purposes
194221 LOG .debug ("Current working directory: {}" ,
@@ -230,12 +257,8 @@ private static synchronized void reloadIfRequired() {
230257 }
231258 }
232259
233- private static String expand (String template , Map <String , String > metadata ) {
234- Map <String , Object > allMetadata = CallContext .current ().getContextStringKeyParameters ();
235- if (MapUtils .isNotEmpty (allMetadata )) {
236- allMetadata .putAll (metadata );
237- }
238- if (MapUtils .isEmpty (allMetadata )) {
260+ protected static String expand (String template , Map <String , String > metadata ) {
261+ if (MapUtils .isEmpty (metadata )) {
239262 return template ;
240263 }
241264 String result = template ;
@@ -248,4 +271,39 @@ private static String expand(String template, Map<String, String> metadata) {
248271 }
249272 return result ;
250273 }
274+
275+ public static String getMessage (String errorKey , Map <String , Object > metadata ) {
276+ String template = getTemplateForKey (errorKey );
277+ if (template == null ) {
278+ return errorKey ;
279+ }
280+ Map <String , Object > combinedMetadata = getCombinedMetadataFromErrorTemplate (template , metadata );
281+ return expand (template , getStringMap (combinedMetadata ));
282+ }
283+
284+ public static void updateExceptionResponse (ExceptionResponse response , CloudRuntimeException cre ) {
285+ String key = cre .getMessageKey ();
286+ Map <String , Object > map = cre .getMetadata ();
287+
288+ if (key == null ) {
289+ Throwable cause = cre .getCause ();
290+ if (!(cause instanceof InvalidParameterValueException )) {
291+ return ;
292+ }
293+ InvalidParameterValueException ipve = (InvalidParameterValueException ) cause ;
294+ key = ipve .getMessageKey ();
295+ map = ipve .getMetadata ();
296+ }
297+ response .setErrorTextKey (key );
298+ String template = getTemplateForKey (key );
299+ if (template == null ) {
300+ response .setErrorText (key );
301+ response .setErrorMetadata (getStringMap (map ));
302+ return ;
303+ }
304+ Map <String , Object > combinedMetadata = getCombinedMetadataFromErrorTemplate (template , map );
305+ Map <String , String > stringMap = getStringMap (combinedMetadata );
306+ response .setErrorText (expand (template , stringMap ));
307+ response .setErrorMetadata (stringMap );
308+ }
251309}
0 commit comments