mirror of https://github.com/apache/cloudstack.git
changes
Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
parent
d535dbcd17
commit
1036df831f
|
|
@ -46,7 +46,7 @@ public class ExceptionResponse extends BaseResponse {
|
|||
|
||||
@SerializedName("errortextkey")
|
||||
@Param(description = "the key for the text associated with this error")
|
||||
private String errorTextKey = "";
|
||||
private String errorTextKey;
|
||||
|
||||
@SerializedName("errormetadata")
|
||||
@Param(description = "the metadata associated with this error")
|
||||
|
|
|
|||
|
|
@ -22,9 +22,13 @@ import java.io.InputStream;
|
|||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.cloudstack.api.Identity;
|
||||
import org.apache.cloudstack.api.InternalIdentity;
|
||||
|
|
@ -41,14 +45,15 @@ import com.cloud.utils.exception.CloudRuntimeException;
|
|||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public final class ErrorMessageResolver {
|
||||
|
||||
public class ErrorMessageResolver {
|
||||
private static final Logger LOG =
|
||||
LogManager.getLogger(ErrorMessageResolver.class);
|
||||
|
||||
private static final String ERROR_MESSAGES_FILENAME = "error-messages.json";
|
||||
private static final String ERROR_KEY_ADMIN_SUFFIX = ".admin";
|
||||
private static final boolean INCLUDE_METADATA_ID_IN_MESSAGE = false;
|
||||
protected static final String ERROR_MESSAGES_FILENAME = "error-messages.json";
|
||||
protected static final String ERROR_KEY_ADMIN_SUFFIX = ".admin";
|
||||
protected static final boolean INCLUDE_METADATA_ID_IN_MESSAGE = false;
|
||||
|
||||
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{\\s*([A-Za-z0-9_]+)\\s*\\}\\}");
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
|
|
@ -61,11 +66,43 @@ public final class ErrorMessageResolver {
|
|||
private ErrorMessageResolver() {
|
||||
}
|
||||
|
||||
public static String getMessage(String errorKey, Map<String, Object> metadata) {
|
||||
return getMessageUsingStringMap(errorKey, getStringMap(metadata));
|
||||
protected static List<String> getVariableNamesInErrorKey(String template) {
|
||||
if (template == null || template.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> variables = new ArrayList<>();
|
||||
Matcher matcher = VARIABLE_PATTERN.matcher(template);
|
||||
while (matcher.find()) {
|
||||
String name = matcher.group(1);
|
||||
if (name != null && !name.isEmpty()) {
|
||||
variables.add(name);
|
||||
}
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
|
||||
private static String getTemplateForKey(String errorKey) {
|
||||
protected static Map<String, Object> getCombinedMetadataFromErrorTemplate(String template, Map<String, Object> metadata) {
|
||||
List<String> variableNames = getVariableNamesInErrorKey(template);
|
||||
if (variableNames.isEmpty()) {
|
||||
return metadata;
|
||||
}
|
||||
Map<String, Object> contextMetadata = CallContext.current().getContextStringKeyParameters();
|
||||
if (MapUtils.isEmpty(contextMetadata)) {
|
||||
return metadata;
|
||||
}
|
||||
Map<String, Object> combinedMetadata = new LinkedHashMap<>();
|
||||
for (String varName : variableNames) {
|
||||
if (contextMetadata.containsKey(varName)) {
|
||||
combinedMetadata.put(varName, contextMetadata.get(varName));
|
||||
}
|
||||
}
|
||||
if (MapUtils.isNotEmpty(metadata)) {
|
||||
combinedMetadata.putAll(metadata);
|
||||
}
|
||||
return combinedMetadata;
|
||||
}
|
||||
|
||||
protected static String getTemplateForKey(String errorKey) {
|
||||
if (errorKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -79,15 +116,7 @@ public final class ErrorMessageResolver {
|
|||
return templates.get(errorKey);
|
||||
}
|
||||
|
||||
private static String getMessageUsingStringMap(String errorKey, Map<String, String> metadata) {
|
||||
String template = getTemplateForKey(errorKey);
|
||||
if (template == null) {
|
||||
return errorKey;
|
||||
}
|
||||
return expand(template, metadata);
|
||||
}
|
||||
|
||||
private static Map<String, String> getStringMap(Map<String, Object> metadata) {
|
||||
protected static Map<String, String> getStringMap(Map<String, Object> metadata) {
|
||||
Map<String, String> stringMap = new LinkedHashMap<>();
|
||||
if (MapUtils.isNotEmpty(metadata)) {
|
||||
for (Map.Entry<String, Object> entry : metadata.entrySet()) {
|
||||
|
|
@ -98,16 +127,35 @@ public final class ErrorMessageResolver {
|
|||
return stringMap;
|
||||
}
|
||||
|
||||
private static String getMetadataObjectStringValue(Object obj) {
|
||||
/**
|
||||
* Converts a metadata object to a human-readable string for error messages.
|
||||
*
|
||||
* <p>Behavior:
|
||||
* <ul>
|
||||
* <li>If {@code obj} is {@code null}, returns {@code null}.</li>
|
||||
* <li>Attempts to obtain a display name by invoking one of the getters
|
||||
* {@code getDisplayText()}, {@code getDisplayName()}, or {@code getName()} via reflection.
|
||||
* If a name is found, returns it quoted as {@code 'NAME'}.</li>
|
||||
* <li>When the current calling account is a root admin, the returned value will include
|
||||
* an identifier suffix in the form {@code (ID: id, UUID: uuid)} when available.
|
||||
* The ID is included only if {@code INCLUDE_METADATA_ID_IN_MESSAGE} is {@code true}
|
||||
* and {@code obj} implements {@link InternalIdentity}. The UUID is included when
|
||||
* {@code obj} implements {@link org.apache.cloudstack.api.Identity}.</li>
|
||||
* <li>If no display name is available, returns the UUID (if {@code obj} implements
|
||||
* {@code Identity}); otherwise returns {@code obj.toString()}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Reflection is used to call getters; invocation failures are silently ignored and treated as
|
||||
* absence of the corresponding value.
|
||||
*
|
||||
* @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 getMetadataObjectStringValue(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
// obj is of primitive type
|
||||
// String value structure should be 'NAME' (ID: id, UUID: uuid)
|
||||
// NAME is obtained from obj.getName() or obj.getDisplayText()
|
||||
// ID is obtained from obj.getId() if obj instanceof InternalIdentity and only for root admin
|
||||
// UUID is obtained from obj.getUuid() if obj instanceof Identity
|
||||
// If NAME is not available, fallback to obj.toString() then simply return UUID if available
|
||||
String uuid = null;
|
||||
if (obj instanceof Identity) {
|
||||
uuid = ((Identity) obj).getUuid();
|
||||
|
|
@ -156,7 +204,7 @@ public final class ErrorMessageResolver {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String invokeStringGetter(Object obj, String methodName) {
|
||||
protected static String invokeStringGetter(Object obj, String methodName) {
|
||||
try {
|
||||
Class<?> cls = obj.getClass();
|
||||
var m = cls.getMethod(methodName);
|
||||
|
|
@ -167,28 +215,7 @@ public final class ErrorMessageResolver {
|
|||
}
|
||||
}
|
||||
|
||||
public static void updateExceptionResponse(ExceptionResponse response, CloudRuntimeException cre) {
|
||||
String key = cre.getMessageKey();
|
||||
Map<String, Object> map = cre.getMetadata();
|
||||
|
||||
if (key == null) {
|
||||
if (cre.getCause() instanceof InvalidParameterValueException) {
|
||||
key = ((InvalidParameterValueException) cre.getCause()).getMessageKey();
|
||||
map = ((InvalidParameterValueException) cre.getCause()).getMetadata();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
response.setErrorTextKey(key);
|
||||
Map<String, String> stringMap = getStringMap(map);
|
||||
String message = getMessageUsingStringMap(key, stringMap);
|
||||
if (message != null) {
|
||||
response.setErrorText(message);
|
||||
}
|
||||
response.setErrorMetadata(stringMap);
|
||||
}
|
||||
|
||||
private static synchronized void reloadIfRequired() {
|
||||
protected static synchronized void reloadIfRequired() {
|
||||
try {
|
||||
// log current directory for debugging purposes
|
||||
LOG.debug("Current working directory: {}",
|
||||
|
|
@ -230,12 +257,8 @@ public final class ErrorMessageResolver {
|
|||
}
|
||||
}
|
||||
|
||||
private static String expand(String template, Map<String, String> metadata) {
|
||||
Map<String, Object> allMetadata = CallContext.current().getContextStringKeyParameters();
|
||||
if (MapUtils.isNotEmpty(allMetadata)) {
|
||||
allMetadata.putAll(metadata);
|
||||
}
|
||||
if (MapUtils.isEmpty(allMetadata)) {
|
||||
protected static String expand(String template, Map<String, String> metadata) {
|
||||
if (MapUtils.isEmpty(metadata)) {
|
||||
return template;
|
||||
}
|
||||
String result = template;
|
||||
|
|
@ -248,4 +271,39 @@ public final class ErrorMessageResolver {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String getMessage(String errorKey, Map<String, Object> metadata) {
|
||||
String template = getTemplateForKey(errorKey);
|
||||
if (template == null) {
|
||||
return errorKey;
|
||||
}
|
||||
Map<String, Object> combinedMetadata = getCombinedMetadataFromErrorTemplate(template, metadata);
|
||||
return expand(template, getStringMap(combinedMetadata));
|
||||
}
|
||||
|
||||
public static void updateExceptionResponse(ExceptionResponse response, CloudRuntimeException cre) {
|
||||
String key = cre.getMessageKey();
|
||||
Map<String, Object> map = cre.getMetadata();
|
||||
|
||||
if (key == null) {
|
||||
Throwable cause = cre.getCause();
|
||||
if (!(cause instanceof InvalidParameterValueException)) {
|
||||
return;
|
||||
}
|
||||
InvalidParameterValueException ipve = (InvalidParameterValueException) cause;
|
||||
key = ipve.getMessageKey();
|
||||
map = ipve.getMetadata();
|
||||
}
|
||||
response.setErrorTextKey(key);
|
||||
String template = getTemplateForKey(key);
|
||||
if (template == null) {
|
||||
response.setErrorText(key);
|
||||
response.setErrorMetadata(getStringMap(map));
|
||||
return;
|
||||
}
|
||||
Map<String, Object> combinedMetadata = getCombinedMetadataFromErrorTemplate(template, map);
|
||||
Map<String, String> stringMap = getStringMap(combinedMetadata);
|
||||
response.setErrorText(expand(template, stringMap));
|
||||
response.setErrorMetadata(stringMap);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,460 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package org.apache.cloudstack.context;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.cloudstack.api.Identity;
|
||||
import org.apache.cloudstack.api.response.ExceptionResponse;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import com.cloud.dc.DataCenter;
|
||||
import com.cloud.exception.InvalidParameterValueException;
|
||||
import com.cloud.utils.PropertiesUtil;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ErrorMessageResolverTest {
|
||||
|
||||
@Mock
|
||||
CallContext callContextMock;
|
||||
|
||||
MockedStatic<CallContext> callContextMocked;
|
||||
MockedStatic<PropertiesUtil> propertiesUtilMocked;
|
||||
|
||||
Path tmpFile;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
callContextMocked = Mockito.mockStatic(CallContext.class);
|
||||
callContextMocked.when(CallContext::current).thenReturn(callContextMock);
|
||||
propertiesUtilMocked = Mockito.mockStatic(PropertiesUtil.class);
|
||||
propertiesUtilMocked.when(() -> PropertiesUtil.findConfigFile(anyString())).thenReturn(null);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
callContextMocked.close();
|
||||
propertiesUtilMocked.close();
|
||||
if (tmpFile != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tmpFile);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getVariableNamesInErrorKey_shouldReturnEmptyListForTemplateWithoutVariables() {
|
||||
String template = "This is a template without variables.";
|
||||
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
|
||||
Assert.assertTrue("Result should be an empty list", result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getVariableNamesInErrorKey_shouldExtractSingleVariable() {
|
||||
String template = "This is a template with one variable: {{variable}}.";
|
||||
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
|
||||
Assert.assertEquals("Result should contain one variable", 1, result.size());
|
||||
Assert.assertEquals("Variable name should be 'variable'", "variable", result.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getVariableNamesInErrorKey_shouldExtractMultipleVariables() {
|
||||
String template = "This template has {{var1}} and {{var2}}.";
|
||||
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
|
||||
Assert.assertEquals("Result should contain two variables", 2, result.size());
|
||||
Assert.assertEquals("First variable name should be 'var1'", "var1", result.get(0));
|
||||
Assert.assertEquals("Second variable name should be 'var2'", "var2", result.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getVariableNamesInErrorKey_shouldIgnoreMalformedVariables() {
|
||||
String template = "This template has {{var1} and {{var2}}.";
|
||||
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
|
||||
Assert.assertEquals("Result should contain one valid variable", 1, result.size());
|
||||
Assert.assertEquals("Variable name should be 'var2'", "var2", result.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getVariableNamesInErrorKey_shouldHandleEmptyTemplate() {
|
||||
String template = "";
|
||||
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
|
||||
Assert.assertTrue("Result should be an empty list", result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getVariableNamesInErrorKey_shouldHandleTemplateWithOnlyVariables() {
|
||||
String template = "{{var1}}{{var2}}{{var3}}";
|
||||
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
|
||||
Assert.assertEquals("Result should contain three variables", 3, result.size());
|
||||
Assert.assertEquals("First variable name should be 'var1'", "var1", result.get(0));
|
||||
Assert.assertEquals("Second variable name should be 'var2'", "var2", result.get(1));
|
||||
Assert.assertEquals("Third variable name should be 'var3'", "var3", result.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCombinedMetadataFromErrorTemplate_shouldReturnMetadataWhenNoVariablesInTemplate() {
|
||||
String template = "This is a template without variables.";
|
||||
Map<String, Object> metadata = Map.of("key1", "value1");
|
||||
Map<String, Object> result = ErrorMessageResolver.getCombinedMetadataFromErrorTemplate(template, metadata);
|
||||
Assert.assertEquals("Result should match the input metadata", metadata, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCombinedMetadataFromErrorTemplate_shouldReturnEmptyMapWhenContextMetadataIsEmpty() {
|
||||
String template = "This template has {{var1}}.";
|
||||
Map<String, Object> metadata = Map.of();
|
||||
when(callContextMock.getContextStringKeyParameters()).thenReturn(Map.of());
|
||||
Map<String, Object> result = ErrorMessageResolver.getCombinedMetadataFromErrorTemplate(template, metadata);
|
||||
Assert.assertTrue("Result should be an empty map", result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCombinedMetadataFromErrorTemplate_shouldCombineContextAndProvidedMetadata() {
|
||||
String template = "This template has {{var1}} and {{var2}}.";
|
||||
Map<String, Object> metadata = Map.of("key1", "value1");
|
||||
when(callContextMock.getContextStringKeyParameters()).thenReturn(Map.of("var1", "valueVar1", "var2", "valueVar2"));
|
||||
Map<String, Object> result = ErrorMessageResolver.getCombinedMetadataFromErrorTemplate(template, metadata);
|
||||
Assert.assertEquals("Result should contain combined metadata", 3, result.size());
|
||||
Assert.assertEquals("Result should contain context metadata for var1", "valueVar1", result.get("var1"));
|
||||
Assert.assertEquals("Result should contain context metadata for var2", "valueVar2", result.get("var2"));
|
||||
Assert.assertEquals("Result should contain provided metadata", "value1", result.get("key1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCombinedMetadataFromErrorTemplate_shouldIgnoreVariablesNotInContextMetadata() {
|
||||
String template = "This template has {{var1}} and {{var2}}.";
|
||||
Map<String, Object> metadata = Map.of("key1", "value1");
|
||||
when(callContextMock.getContextStringKeyParameters()).thenReturn(Map.of("var1", "valueVar1"));
|
||||
Map<String, Object> result = ErrorMessageResolver.getCombinedMetadataFromErrorTemplate(template, metadata);
|
||||
Assert.assertEquals("Result should contain combined metadata", 2, result.size());
|
||||
Assert.assertEquals("Result should contain context metadata for var1", "valueVar1", result.get("var1"));
|
||||
Assert.assertEquals("Result should contain provided metadata", "value1", result.get("key1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCombinedMetadataFromErrorTemplate_shouldReturnProvidedMetadataWhenTemplateIsEmpty() {
|
||||
String template = "";
|
||||
Map<String, Object> metadata = Map.of("key1", "value1");
|
||||
Map<String, Object> result = ErrorMessageResolver.getCombinedMetadataFromErrorTemplate(template, metadata);
|
||||
Assert.assertEquals("Result should match the input metadata", metadata, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getStringMap_shouldReturnEmptyMapWhenMetadataIsEmpty() {
|
||||
Map<String, Object> metadata = Map.of();
|
||||
Map<String, String> result = ErrorMessageResolver.getStringMap(metadata);
|
||||
Assert.assertTrue("Result should be an empty map", result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getStringMap_shouldConvertAllMetadataValuesToStrings() {
|
||||
Map<String, Object> metadata = Map.of("key1", 123, "key2", true, "key3", "value");
|
||||
Map<String, String> result = ErrorMessageResolver.getStringMap(metadata);
|
||||
Assert.assertEquals("Result should contain all keys", 3, result.size());
|
||||
Assert.assertEquals("Value for key1 should be '123'", "123", result.get("key1"));
|
||||
Assert.assertEquals("Value for key2 should be 'true'", "true", result.get("key2"));
|
||||
Assert.assertEquals("Value for key3 should be 'value'", "value", result.get("key3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getStringMap_shouldHandleNullValuesInMetadata() {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.put("key1", null);
|
||||
metadata.put("key2", "value");
|
||||
Map<String, String> result = ErrorMessageResolver.getStringMap(metadata);
|
||||
Assert.assertEquals("Result should contain all keys", 2, result.size());
|
||||
Assert.assertNull("Value for key1 should be null", result.get("key1"));
|
||||
Assert.assertEquals("Value for key2 should be 'value'", "value", result.get("key2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getStringMap_shouldReturnEmptyMapWhenMetadataIsNull() {
|
||||
Map<String, String> result = ErrorMessageResolver.getStringMap(null);
|
||||
Assert.assertTrue("Result should be an empty map", result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetadataObjectStringValue_shouldReturnNullWhenObjectIsNull() {
|
||||
Assert.assertNull(ErrorMessageResolver.getMetadataObjectStringValue(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetadataObjectStringValue_shouldReturnUuidWhenNameIsUnavailable() {
|
||||
Identity identityMock = Mockito.mock(Identity.class);
|
||||
when(identityMock.getUuid()).thenReturn("uuid-1234");
|
||||
Assert.assertEquals("uuid-1234", ErrorMessageResolver.getMetadataObjectStringValue(identityMock));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetadataObjectStringValue_shouldReturnNameWhenAvailable() {
|
||||
DataCenter identityMock = Mockito.mock(DataCenter.class);
|
||||
when(identityMock.getUuid()).thenReturn("uuid-1234");
|
||||
when(identityMock.getName()).thenReturn("TestName");
|
||||
Assert.assertEquals("'TestName'", ErrorMessageResolver.getMetadataObjectStringValue(identityMock));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetadataObjectStringValue_shouldIncludeIdAndUuidForRootAdmin() {
|
||||
DataCenter internalIdentityMock = Mockito.mock(DataCenter.class);
|
||||
when(internalIdentityMock.getUuid()).thenReturn("uuid-5678");
|
||||
if (ErrorMessageResolver.INCLUDE_METADATA_ID_IN_MESSAGE) {
|
||||
when(internalIdentityMock.getId()).thenReturn(42L);
|
||||
}
|
||||
when(internalIdentityMock.getName()).thenReturn("AdminName");
|
||||
when(CallContext.current().isCallingAccountRootAdmin()).thenReturn(true);
|
||||
String expected = String.format("'AdminName' (%sUUID: uuid-5678)", ErrorMessageResolver.INCLUDE_METADATA_ID_IN_MESSAGE ? "ID: 42, " : "");
|
||||
Assert.assertEquals(expected, ErrorMessageResolver.getMetadataObjectStringValue(internalIdentityMock));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetadataObjectStringValue_shouldFallbackToToStringWhenNameAndUuidAreUnavailable() {
|
||||
Object obj = new Object();
|
||||
Assert.assertEquals(obj.toString(), ErrorMessageResolver.getMetadataObjectStringValue(obj));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetadataObjectStringValue_shouldReturnNameOnlyForNonRootAdmin() {
|
||||
DataCenter internalIdentityMock = Mockito.mock(DataCenter.class);
|
||||
when(internalIdentityMock.getName()).thenReturn("UserName");
|
||||
when(CallContext.current().isCallingAccountRootAdmin()).thenReturn(false);
|
||||
Assert.assertEquals("'UserName'", ErrorMessageResolver.getMetadataObjectStringValue(internalIdentityMock));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expand_shouldReturnTemplateWhenMetadataIsEmpty() {
|
||||
String template = "This is a template with no placeholders.";
|
||||
Map<String, String> metadata = Map.of();
|
||||
String result = ErrorMessageResolver.expand(template, metadata);
|
||||
Assert.assertEquals("This is a template with no placeholders.", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expand_shouldReplaceSinglePlaceholderWithMetadataValue() {
|
||||
String template = "Hello, {{name}}!";
|
||||
Map<String, String> metadata = Map.of("name", "World");
|
||||
String result = ErrorMessageResolver.expand(template, metadata);
|
||||
Assert.assertEquals("Hello, World!", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expand_shouldReplaceMultiplePlaceholdersWithMetadataValues() {
|
||||
String template = "Hello, {{name}}! Today is {{day}}.";
|
||||
Map<String, String> metadata = Map.of("name", "Alice", "day", "Monday");
|
||||
String result = ErrorMessageResolver.expand(template, metadata);
|
||||
Assert.assertEquals("Hello, Alice! Today is Monday.", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expand_shouldIgnorePlaceholdersWithoutMatchingMetadata() {
|
||||
String template = "Hello, {{name}}! Your age is {{age}}.";
|
||||
Map<String, String> metadata = Map.of("name", "Bob");
|
||||
String result = ErrorMessageResolver.expand(template, metadata);
|
||||
Assert.assertEquals("Hello, Bob! Your age is {{age}}.", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expand_shouldHandleNullMetadataValues() {
|
||||
String template = "Hello, {{name}}!";
|
||||
Map<String, String> metadata = new LinkedHashMap<>();
|
||||
metadata.put("name", null);
|
||||
String result = ErrorMessageResolver.expand(template, metadata);
|
||||
Assert.assertEquals("Hello, {{name}}!", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expand_shouldReturnTemplateWhenMetadataIsNull() {
|
||||
String template = "This is a template with no placeholders.";
|
||||
String result = ErrorMessageResolver.expand(template, null);
|
||||
Assert.assertEquals("This is a template with no placeholders.", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expand_shouldHandleTemplateWithNoPlaceholders() {
|
||||
String template = "This template has no placeholders.";
|
||||
Map<String, String> metadata = Map.of("key", "value");
|
||||
String result = ErrorMessageResolver.expand(template, metadata);
|
||||
Assert.assertEquals("This template has no placeholders.", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMessage_shouldReturnErrorKeyWhenTemplateIsNull() {
|
||||
String errorKey = "error.key";
|
||||
String result = ErrorMessageResolver.getMessage(errorKey, Map.of());
|
||||
Assert.assertEquals(errorKey, result);
|
||||
}
|
||||
|
||||
private void createTempFileWithTemplate(String errorKey, String template) {
|
||||
try {
|
||||
tmpFile = Files.createTempFile("error-template-", ".txt");
|
||||
Files.writeString(tmpFile, String.format("{ \"%s\": \"%s\" }", errorKey, template));
|
||||
propertiesUtilMocked.when(() -> PropertiesUtil.findConfigFile(anyString()))
|
||||
.thenReturn(tmpFile.toFile());
|
||||
} catch (IOException e) {
|
||||
Assert.fail("Failed to create temporary file for testing: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMessage_shouldReturnExpandedMessageWhenTemplateExists() {
|
||||
String errorKey = "error.key";
|
||||
String template = "Error occurred: {{field}}";
|
||||
Map<String, Object> metadata = Map.of("field", "value");
|
||||
createTempFileWithTemplate(errorKey, template);
|
||||
when(callContextMock.getContextStringKeyParameters()).thenReturn(Map.of());
|
||||
String result = ErrorMessageResolver.getMessage(errorKey, metadata);
|
||||
Assert.assertEquals("Error occurred: value", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMessage_shouldHandleEmptyMetadata() {
|
||||
String errorKey = "error.key";
|
||||
String template = "Error occurred.";
|
||||
createTempFileWithTemplate(errorKey, template);
|
||||
String result = ErrorMessageResolver.getMessage(errorKey, Map.of());
|
||||
Assert.assertEquals("Error occurred.", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMessage_shouldHandleNullMetadata() {
|
||||
String errorKey = "error.key";
|
||||
String template = "Error occurred.";
|
||||
createTempFileWithTemplate(errorKey, template);
|
||||
String result = ErrorMessageResolver.getMessage(errorKey, null);
|
||||
Assert.assertEquals("Error occurred.", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMessage_shouldCombineContextAndProvidedMetadata() {
|
||||
String errorKey = "error.key";
|
||||
String template = "Error in {{field1}} and {{field2}}.";
|
||||
Map<String, Object> metadata = Map.of("field1", "value1");
|
||||
createTempFileWithTemplate(errorKey, template);
|
||||
when(callContextMock.getContextStringKeyParameters()).thenReturn(Map.of("field2", "value2"));
|
||||
String result = ErrorMessageResolver.getMessage(errorKey, metadata);
|
||||
Assert.assertEquals("Error in value1 and value2.", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateExceptionResponse_shouldSetErrorTextAndMetadataWhenKeyAndTemplateExist() {
|
||||
ExceptionResponse response = new ExceptionResponse();
|
||||
CloudRuntimeException cre = Mockito.mock(CloudRuntimeException.class);
|
||||
String key = "error.key";
|
||||
String template = "Error occurred: {{field}}";
|
||||
Map<String, Object> metadata = Map.of("field", "value");
|
||||
|
||||
when(cre.getMessageKey()).thenReturn(key);
|
||||
when(cre.getMetadata()).thenReturn(metadata);
|
||||
createTempFileWithTemplate(key, template);
|
||||
|
||||
ErrorMessageResolver.updateExceptionResponse(response, cre);
|
||||
|
||||
Assert.assertEquals(key, response.getErrorTextKey());
|
||||
Assert.assertEquals("Error occurred: value", response.getErrorText());
|
||||
Assert.assertEquals(Map.of("field", "value"), response.getErrorMetadata());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateExceptionResponse_shouldSetErrorTextKeyOnlyWhenTemplateDoesNotExist() {
|
||||
ExceptionResponse response = new ExceptionResponse();
|
||||
CloudRuntimeException cre = Mockito.mock(CloudRuntimeException.class);
|
||||
String key = "error.key";
|
||||
Map<String, Object> metadata = Map.of("field", "value");
|
||||
|
||||
when(cre.getMessageKey()).thenReturn(key);
|
||||
when(cre.getMetadata()).thenReturn(metadata);
|
||||
|
||||
ErrorMessageResolver.updateExceptionResponse(response, cre);
|
||||
|
||||
Assert.assertEquals(key, response.getErrorTextKey());
|
||||
Assert.assertEquals(key, response.getErrorText());
|
||||
Assert.assertEquals(Map.of("field", "value"), response.getErrorMetadata());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateExceptionResponse_shouldHandleNullKeyAndCauseIsNotInvalidParameterValueException() {
|
||||
ExceptionResponse response = new ExceptionResponse();
|
||||
String originalErrorText = response.getErrorText();
|
||||
CloudRuntimeException cre = Mockito.mock(CloudRuntimeException.class);
|
||||
|
||||
when(cre.getMessageKey()).thenReturn(null);
|
||||
when(cre.getCause()).thenReturn(new RuntimeException());
|
||||
|
||||
ErrorMessageResolver.updateExceptionResponse(response, cre);
|
||||
|
||||
Assert.assertNull(response.getErrorTextKey());
|
||||
Assert.assertNull(response.getErrorMetadata());
|
||||
Assert.assertEquals(originalErrorText, response.getErrorText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateExceptionResponse_shouldUseCauseMetadataWhenKeyIsNullAndCauseIsInvalidParameterValueException() {
|
||||
ExceptionResponse response = new ExceptionResponse();
|
||||
InvalidParameterValueException cause = Mockito.mock(InvalidParameterValueException.class);
|
||||
CloudRuntimeException cre = Mockito.mock(CloudRuntimeException.class);
|
||||
String key = "error.key";
|
||||
String template = "Error occurred: {{field}}";
|
||||
Map<String, Object> metadata = Map.of("field", "value");
|
||||
|
||||
when(cre.getMessageKey()).thenReturn(null);
|
||||
when(cre.getCause()).thenReturn(cause);
|
||||
when(cause.getMessageKey()).thenReturn(key);
|
||||
when(cause.getMetadata()).thenReturn(metadata);
|
||||
createTempFileWithTemplate(key, template);
|
||||
|
||||
ErrorMessageResolver.updateExceptionResponse(response, cre);
|
||||
|
||||
Assert.assertEquals(key, response.getErrorTextKey());
|
||||
Assert.assertEquals("Error occurred: value", response.getErrorText());
|
||||
Assert.assertEquals(Map.of("field", "value"), response.getErrorMetadata());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateExceptionResponse_shouldHandleNullMetadata() {
|
||||
ExceptionResponse response = new ExceptionResponse();
|
||||
CloudRuntimeException cre = Mockito.mock(CloudRuntimeException.class);
|
||||
String key = "error.key";
|
||||
String template = "Error occurred.";
|
||||
|
||||
when(cre.getMessageKey()).thenReturn(key);
|
||||
when(cre.getMetadata()).thenReturn(null);
|
||||
createTempFileWithTemplate(key, template);
|
||||
|
||||
ErrorMessageResolver.updateExceptionResponse(response, cre);
|
||||
|
||||
Assert.assertEquals(key, response.getErrorTextKey());
|
||||
Assert.assertEquals("Error occurred.", response.getErrorText());
|
||||
Assert.assertTrue(response.getErrorMetadata().isEmpty());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,21 @@
|
|||
{
|
||||
"vm.deploy.network.not.found.ip.map": "The network selected {{networkId}} in IP to network map could not be found. It may have been removed or is no longer accessible.",
|
||||
"vm.deploy.serviceoffering.fixed.parameters.not.allowed": "Unable to deploy the instance because the selected service offering {{serviceOffering}} does not allow specifying {{cpuNumberKey}}, {{cpuSpeedKey}}, or {{memory}}.",
|
||||
"vm.deploy.serviceoffering.fixed.parameters.not.allowed.admin": "Unable to deploy the instance because the selected service offering {{serviceOffering}} is not a dynamic offering and it does not allow specifying {{cpuNumberKey}}, {{cpuSpeedKey}}, or {{memory}}.",
|
||||
"vm.deploy.serviceoffering.inactive": "Unable to deploy Instance as the given service offering {{serviceOffering}} is inactive. Specify an active service offering.",
|
||||
"vm.deploy.serviceoffering.not.specified": "Unable to deploy Instance as the required parameter 'serviceofferingid' is missing.",
|
||||
"vm.deploy.serviceoffering.override.not.allowed": "Unable to deploy Instance as the selected service offering {{serviceOffering}} does not allow changing the disk offering.",
|
||||
"vm.deploy.serviceoffering.override.not.allowed.admin": "Unable to deploy Instance as the selected service offering {{serviceOffering}} uses disk offering strictness and does not allow changing the disk offering."
|
||||
"vm.deploy.diskoffering.compute.only": "Unable to deploy Instance as the specified disk offering {{diskOffering}} is mapped to a service offering. Specify a disk offering that is not mapped to any service offering.",
|
||||
"vm.deploy.diskoffering.local.storage.zone.unsupported": "Unable to deploy Instance because the zone is not configured to use local storage, but the specified disk offering {{diskOffering}} requires it.",
|
||||
"vm.deploy.diskoffering.not.found": "Unable to deploy Instance as the specified disk offering is not found.",
|
||||
"vm.deploy.diskoffering.with.diskoffering.details": "Unable to deploy Instance as both a disk offering ID and data disk offering details were provided. Specify only one.",
|
||||
"vm.deploy.hypervisor.volume.snapshot.not.supported": "Unable to deploy Instance because deployment from an existing volume or snapshot is supported only on the KVM hypervisor.",
|
||||
"vm.deploy.network.not.found.ip.map": "The network selected {{networkId}} in IP to network map could not be found. It may have been removed or is no longer accessible.",
|
||||
"vm.deploy.serviceoffering.fixed.parameters.not.allowed": "Unable to deploy the instance because the selected service offering {{serviceOffering}} does not allow specifying {{cpuNumberKey}}, {{cpuSpeedKey}}, or {{memory}}.",
|
||||
"vm.deploy.serviceoffering.fixed.parameters.not.allowed.admin": "Unable to deploy the instance because the selected service offering {{serviceOffering}} is not a dynamic offering and it does not allow specifying {{cpuNumberKey}}, {{cpuSpeedKey}}, or {{memory}}.",
|
||||
"vm.deploy.serviceoffering.inactive": "Unable to deploy Instance as the given service offering {{serviceOffering}} is inactive. Specify an active service offering.",
|
||||
"vm.deploy.serviceoffering.local.storage.zone.unsupported": "Unable to deploy Instance because the zone is not configured to use local storage, but the disk offering mapped to service offering {{serviceOffering}} requires it.",
|
||||
"vm.deploy.serviceoffering.not.specified": "Unable to deploy Instance as the required parameter 'serviceofferingid' is missing.",
|
||||
"vm.deploy.serviceoffering.not.found": "Unable to deploy Instance as the specified service offering is not found.",
|
||||
"vm.deploy.serviceoffering.override.not.allowed": "Unable to deploy Instance as the selected service offering {{serviceOffering}} does not allow changing the disk offering.",
|
||||
"vm.deploy.serviceoffering.override.not.allowed.admin": "Unable to deploy Instance as the selected service offering {{serviceOffering}} uses disk offering strictness and does not allow changing the disk offering.",
|
||||
"vm.deploy.snapshot.not.found": "Unable to deploy Instance as the specified Snapshot is not found.",
|
||||
"vm.deploy.template.associated.not.usable": "Unable to deploy Instance as the associated Template cannot be used.",
|
||||
"vm.deploy.template.not.found": "Unable to deploy Instance as the specified Template is not found.",
|
||||
"vm.deploy.volume.not.found": "Unable to deploy Instance as the specified Volume is not found.",
|
||||
"vm.deploy.zone.not.found": "Unable to deploy Instance as the specified zone is not found."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6383,7 +6383,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
|
|||
|
||||
DataCenter zone = _entityMgr.findById(DataCenter.class, zoneId);
|
||||
if (zone == null) {
|
||||
throw new InvalidParameterValueException("Unable to find zone by id=" + zoneId);
|
||||
throw new InvalidParameterValueException("vm.deploy.zone.not.found", Collections.emptyMap());
|
||||
}
|
||||
|
||||
Long serviceOfferingId = cmd.getServiceOfferingId();
|
||||
|
|
@ -6394,7 +6394,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
|
|||
|
||||
ServiceOffering serviceOffering = _entityMgr.findById(ServiceOffering.class, serviceOfferingId);
|
||||
if (serviceOffering == null) {
|
||||
throw new InvalidParameterValueException("Unable to find service offering: " + serviceOffering.getId());
|
||||
throw new InvalidParameterValueException("vm.deploy.serviceoffering.not.found", Collections.emptyMap());
|
||||
}
|
||||
verifyServiceOffering(cmd, serviceOffering);
|
||||
|
||||
|
|
@ -6408,14 +6408,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
|
|||
if (cmd.getVolumeId() != null) {
|
||||
volume = getVolume(cmd.getVolumeId(), templateId, false);
|
||||
if (volume == null) {
|
||||
throw new InvalidParameterValueException("Could not find volume with id=" + cmd.getVolumeId());
|
||||
throw new InvalidParameterValueException("vm.deploy.volume.not.found", Collections.emptyMap());
|
||||
}
|
||||
_accountMgr.checkAccess(caller, null, true, volume);
|
||||
templateId = volume.getTemplateId();
|
||||
} else if (cmd.getSnapshotId() != null) {
|
||||
snapshot = _snapshotDao.findById(cmd.getSnapshotId());
|
||||
if (snapshot == null) {
|
||||
throw new InvalidParameterValueException("Could not find snapshot with id=" + cmd.getSnapshotId());
|
||||
throw new InvalidParameterValueException("vm.deploy.snapshot.not.found", Collections.emptyMap());
|
||||
}
|
||||
_accountMgr.checkAccess(caller, null, true, snapshot);
|
||||
VolumeInfo volumeOfSnapshot = getVolume(snapshot.getVolumeId(), templateId, true);
|
||||
|
|
@ -6431,16 +6431,18 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
|
|||
VirtualMachineTemplate template = null;
|
||||
if (volume != null || snapshot != null) {
|
||||
template = _entityMgr.findByIdIncludingRemoved(VirtualMachineTemplate.class, templateId);
|
||||
if (template == null) {
|
||||
throw new InvalidParameterValueException("vm.deploy.template.associated.not.usable", Collections.emptyMap());
|
||||
}
|
||||
} else {
|
||||
template = _entityMgr.findById(VirtualMachineTemplate.class, templateId);
|
||||
if (template == null) {
|
||||
throw new InvalidParameterValueException("vm.deploy.template.not.found", Collections.emptyMap());
|
||||
}
|
||||
}
|
||||
if (cmd.isVolumeOrSnapshotProvided() &&
|
||||
(!(HypervisorType.KVM.equals(template.getHypervisorType()) || HypervisorType.KVM.equals(cmd.getHypervisor())))) {
|
||||
throw new InvalidParameterValueException("Deploying a virtual machine with existing volume/snapshot is supported only from KVM hypervisors");
|
||||
}
|
||||
// Make sure a valid template ID was specified
|
||||
if (template == null) {
|
||||
throw new InvalidParameterValueException("Unable to use template " + templateId);
|
||||
throw new InvalidParameterValueException("vm.deploy.hypervisor.volume.snapshot.not.supported", Collections.emptyMap());
|
||||
}
|
||||
verifyTemplate(cmd, template, serviceOfferingId);
|
||||
|
||||
|
|
@ -6449,25 +6451,29 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
|
|||
if (diskOfferingId != null) {
|
||||
diskOffering = _entityMgr.findById(DiskOffering.class, diskOfferingId);
|
||||
if (diskOffering == null) {
|
||||
throw new InvalidParameterValueException("Unable to find disk offering " + diskOfferingId);
|
||||
throw new InvalidParameterValueException("vm.deploy.diskoffering.not.found", Collections.emptyMap());
|
||||
}
|
||||
if (diskOffering.isComputeOnly()) {
|
||||
throw new InvalidParameterValueException(String.format("The disk offering %s provided is directly mapped to a service offering, please provide an individual disk offering", diskOffering));
|
||||
throw new InvalidParameterValueException("vm.deploy.diskoffering.compute.only",
|
||||
Map.of("diskOffering", diskOffering));
|
||||
}
|
||||
}
|
||||
|
||||
List<VmDiskInfo> dataDiskInfoList = cmd.getDataDiskInfoList();
|
||||
if (dataDiskInfoList != null && diskOfferingId != null) {
|
||||
new InvalidParameterValueException("Cannot specify both disk offering id and data disk offering details");
|
||||
throw new InvalidParameterValueException("vm.deploy.diskoffering.with.diskoffering.details",
|
||||
Collections.emptyMap());
|
||||
}
|
||||
|
||||
if (!zone.isLocalStorageEnabled()) {
|
||||
DiskOffering diskOfferingMappedInServiceOffering = _entityMgr.findById(DiskOffering.class, serviceOffering.getDiskOfferingId());
|
||||
if (diskOfferingMappedInServiceOffering.isUseLocalStorage()) {
|
||||
throw new InvalidParameterValueException("Zone is not configured to use local storage but disk offering " + diskOfferingMappedInServiceOffering.getName() + " mapped in service offering uses it");
|
||||
throw new InvalidParameterValueException("vm.deploy.serviceoffering.local.storage.zone.unsupported",
|
||||
Map.of("serviceOffering", serviceOffering));
|
||||
}
|
||||
if (diskOffering != null && diskOffering.isUseLocalStorage()) {
|
||||
throw new InvalidParameterValueException("Zone is not configured to use local storage but disk offering " + diskOffering.getName() + " uses it");
|
||||
throw new InvalidParameterValueException("vm.deploy.diskoffering.local.storage.zone.unsupported",
|
||||
Map.of("diskOffering", diskOffering));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue