Revert "CLOUDSTACK-6003 fixing plus refactoring dispatcher" as it breaks API dispatching for commands having Map<String,String> as a parameter type
This reverts commit 447430c3df38c36d947c44c4aebd961d8cbb14c4. Conflicts: api/src/org/apache/cloudstack/api/BaseCmd.java server/src/com/cloud/api/ApiDispatcher.java server/src/com/cloud/network/as/AutoScaleManagerImpl.java server/src/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/782c5306 Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/782c5306 Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/782c5306 Branch: refs/heads/rbac Commit: 782c53068564b96d11a09775d489a6e1b38adbd8 Parents: 9167286 Author: Alena Prokharchyk <alena.prokharc...@citrix.com> Authored: Thu Feb 6 14:34:43 2014 -0800 Committer: Alena Prokharchyk <alena.prokharc...@citrix.com> Committed: Thu Feb 6 14:46:58 2014 -0800 ---------------------------------------------------------------------- .../org/apache/cloudstack/api/ApiConstants.java | 5 - api/src/org/apache/cloudstack/api/BaseCmd.java | 155 +++--- .../org/apache/cloudstack/api/BaseListCmd.java | 30 +- .../autoscale/CreateAutoScaleVmProfileCmd.java | 30 +- .../core/spring-server-core-misc-context.xml | 10 - .../com/cloud/api/ApiAsyncJobDispatcher.java | 26 +- server/src/com/cloud/api/ApiDispatcher.java | 485 +++++++++++++++++- server/src/com/cloud/api/ApiServer.java | 345 ++++++------- server/src/com/cloud/api/ApiServlet.java | 134 ++--- server/src/com/cloud/api/ParameterHandler.java | 138 ------ .../api/dispatch/CommandCreationWorker.java | 55 --- .../com/cloud/api/dispatch/DispatchChain.java | 45 -- .../api/dispatch/DispatchChainFactory.java | 68 --- .../com/cloud/api/dispatch/DispatchWorker.java | 34 -- .../dispatch/ParamGenericValidationWorker.java | 95 ---- .../cloud/api/dispatch/ParamProcessWorker.java | 429 ---------------- .../dispatch/ParamSemanticValidationWorker.java | 40 -- .../cloud/network/as/AutoScaleManagerImpl.java | 490 +++++++++---------- .../VirtualNetworkApplianceManagerImpl.java | 14 +- .../storage/snapshot/SnapshotSchedulerImpl.java | 107 ++-- .../test/com/cloud/api/ApiDispatcherTest.java | 106 ++++ .../api/dispatch/CommandCreationWorkerTest.java | 48 -- .../api/dispatch/DispatchChainFactoryTest.java | 54 -- .../ParamGenericValidationWorkerTest.java | 163 ------ .../api/dispatch/ParamProcessWorkerTest.java | 107 ---- .../ParamSemanticValidationWorkerTest.java | 48 -- 26 files changed, 1227 insertions(+), 2034 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cloudstack/blob/782c5306/api/src/org/apache/cloudstack/api/ApiConstants.java ---------------------------------------------------------------------- diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 749a814..d500303 100755 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -48,12 +48,10 @@ public class ApiConstants { public static final String CLUSTER_ID = "clusterid"; public static final String CLUSTER_NAME = "clustername"; public static final String CLUSTER_TYPE = "clustertype"; - public static final String COMMAND = "command"; public static final String COMPONENT = "component"; public static final String CPU_NUMBER = "cpunumber"; public static final String CPU_SPEED = "cpuspeed"; public static final String CREATED = "created"; - public static final String CTX_START_EVENT_ID = "ctxStartEventId"; public static final String CUSTOMIZED = "customized"; public static final String CUSTOMIZED_IOPS = "customizediops"; public static final String CUSTOM_ID = "customid"; @@ -80,7 +78,6 @@ public class ApiConstants { public static final String IP6_DNS2 = "ip6dns2"; public static final String DOMAIN = "domain"; public static final String DOMAIN_ID = "domainid"; - public static final String DOMAIN__ID = "domainId"; public static final String DURATION = "duration"; public static final String EMAIL = "email"; public static final String END_DATE = "enddate"; @@ -211,7 +208,6 @@ public class ApiConstants { public static final String SENT = "sent"; public static final String SENT_BYTES = "sentbytes"; public static final String SERVICE_OFFERING_ID = "serviceofferingid"; - public static final String SESSIONKEY = "sessionkey"; public static final String SHOW_CAPACITIES = "showcapacities"; public static final String SHOW_REMOVED = "showremoved"; public static final String SIZE = "size"; @@ -280,7 +276,6 @@ public class ApiConstants { public static final String NETWORKRATE = "networkrate"; public static final String HOST_TAGS = "hosttags"; public static final String SSH_KEYPAIR = "keypair"; - public static final String HTTPMETHOD = "httpmethod"; public static final String HOST_CPU_CAPACITY = "hostcpucapacity"; public static final String HOST_CPU_NUM = "hostcpunum"; public static final String HOST_MEM_CAPACITY = "hostmemcapacity"; http://git-wip-us.apache.org/repos/asf/cloudstack/blob/782c5306/api/src/org/apache/cloudstack/api/BaseCmd.java ---------------------------------------------------------------------- diff --git a/api/src/org/apache/cloudstack/api/BaseCmd.java b/api/src/org/apache/cloudstack/api/BaseCmd.java index 5b50153..0e83cee 100644 --- a/api/src/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/org/apache/cloudstack/api/BaseCmd.java @@ -17,22 +17,17 @@ package org.apache.cloudstack.api; -import java.lang.reflect.Field; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.regex.Pattern; import javax.inject.Inject; -import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.affinity.AffinityGroupService; import org.apache.cloudstack.alert.AlertService; -import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.element.InternalLoadBalancerElementService; import org.apache.cloudstack.network.lb.ApplicationLoadBalancerService; import org.apache.cloudstack.network.lb.InternalLoadBalancerVMService; @@ -79,7 +74,6 @@ import com.cloud.user.Account; import com.cloud.user.AccountService; import com.cloud.user.DomainService; import com.cloud.user.ResourceLimitService; -import com.cloud.utils.ReflectUtil; import com.cloud.utils.db.EntityManager; import com.cloud.utils.db.UUIDManager; import com.cloud.vm.UserVmService; @@ -103,8 +97,6 @@ public abstract class BaseCmd { public static Pattern newInputDateFormat = Pattern.compile("[\\d]+-[\\d]+-[\\d]+ [\\d]+:[\\d]+:[\\d]+"); private static final DateFormat s_outputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); - protected static final Map<Class<?>, List<Field>> fieldsForCmdClass = new HashMap<Class<?>, List<Field>>(); - private Object _responseObject = null; private Map<String, String> fullUrlParams; @@ -213,7 +205,7 @@ public abstract class BaseCmd { return httpMethod; } - public void setHttpMethod(final String method) { + public void setHttpMethod(String method) { if (method != null) { if (method.equalsIgnoreCase("GET")) httpMethod = HTTPMethod.GET; @@ -235,7 +227,7 @@ public abstract class BaseCmd { return responseType; } - public void setResponseType(final String responseType) { + public void setResponseType(String responseType) { this.responseType = responseType; } @@ -253,7 +245,7 @@ public abstract class BaseCmd { return _responseObject; } - public void setResponseObject(final Object responseObject) { + public void setResponseObject(Object responseObject) { _responseObject = responseObject; } @@ -261,7 +253,7 @@ public abstract class BaseCmd { return _mgr; } - public static String getDateString(final Date date) { + public static String getDateString(Date date) { if (date == null) { return ""; } @@ -272,83 +264,101 @@ public abstract class BaseCmd { return formattedString; } - protected List<Field> getAllFieldsForClass(final Class<?> clazz) { - List<Field> filteredFields = fieldsForCmdClass.get(clazz); + // FIXME: move this to a utils method so that maps can be unpacked and integer/long values can be appropriately cast + @SuppressWarnings({"unchecked", "rawtypes"}) + public Map<String, Object> unpackParams(Map<String, String> params) { + Map<String, Object> lowercaseParams = new HashMap<String, Object>(); + for (String key : params.keySet()) { + int arrayStartIndex = key.indexOf('['); + int arrayStartLastIndex = key.lastIndexOf('['); + if (arrayStartIndex != arrayStartLastIndex) { + throw new ServerApiException(ApiErrorCode.MALFORMED_PARAMETER_ERROR, "Unable to decode parameter " + key + + "; if specifying an object array, please use parameter[index].field=XXX, e.g. userGroupList[0].group=httpGroup"); + } - // If list of fields was not cached yet - if (filteredFields == null) { - final List<Field> allFields = ReflectUtil.getAllFieldsForClass(this.getClass(), BaseCmd.class); - filteredFields = new ArrayList<Field>(); + if (arrayStartIndex > 0) { + int arrayEndIndex = key.indexOf(']'); + int arrayEndLastIndex = key.lastIndexOf(']'); + if ((arrayEndIndex < arrayStartIndex) || (arrayEndIndex != arrayEndLastIndex)) { + // malformed parameter + throw new ServerApiException(ApiErrorCode.MALFORMED_PARAMETER_ERROR, "Unable to decode parameter " + key + + "; if specifying an object array, please use parameter[index].field=XXX, e.g. userGroupList[0].group=httpGroup"); + } - for (final Field field : allFields) { - final Parameter parameterAnnotation = field.getAnnotation(Parameter.class); - if ((parameterAnnotation != null) && parameterAnnotation.expose()) { - filteredFields.add(field); + // Now that we have an array object, check for a field name in the case of a complex object + int fieldIndex = key.indexOf('.'); + String fieldName = null; + if (fieldIndex < arrayEndIndex) { + throw new ServerApiException(ApiErrorCode.MALFORMED_PARAMETER_ERROR, "Unable to decode parameter " + key + + "; if specifying an object array, please use parameter[index].field=XXX, e.g. userGroupList[0].group=httpGroup"); + } else { + fieldName = key.substring(fieldIndex + 1); } - } - // Cache the prepared list for future use - fieldsForCmdClass.put(clazz, filteredFields); - } - return filteredFields; - } + // parse the parameter name as the text before the first '[' character + String paramName = key.substring(0, arrayStartIndex); + paramName = paramName.toLowerCase(); + + Map<Integer, Map> mapArray = null; + Map<String, Object> mapValue = null; + String indexStr = key.substring(arrayStartIndex + 1, arrayEndIndex); + int index = 0; + boolean parsedIndex = false; + try { + if (indexStr != null) { + index = Integer.parseInt(indexStr); + parsedIndex = true; + } + } catch (NumberFormatException nfe) { + s_logger.warn("Invalid parameter " + key + " received, unable to parse object array, returning an error."); + } - protected Account getCurrentContextAccount() { - return CallContext.current().getCallingAccount(); - } + if (!parsedIndex) { + throw new ServerApiException(ApiErrorCode.MALFORMED_PARAMETER_ERROR, "Unable to decode parameter " + key + + "; if specifying an object array, please use parameter[index].field=XXX, e.g. userGroupList[0].group=httpGroup"); + } - /** - * this method doesn't return all the @{link Parameter}, but only the ones exposed - * and allowed for current @{link RoleType} - * - * @return - */ - public List<Field> getParamFields() { - final List<Field> allFields = getAllFieldsForClass(this.getClass()); - final List<Field> validFields = new ArrayList<Field>(); - final Account caller = getCurrentContextAccount(); - - for (final Field field : allFields) { - final Parameter parameterAnnotation = field.getAnnotation(Parameter.class); - - //TODO: Annotate @Validate on API Cmd classes, FIXME how to process Validate - final RoleType[] allowedRoles = parameterAnnotation.authorized(); - boolean roleIsAllowed = true; - if (allowedRoles.length > 0) { - roleIsAllowed = false; - for (final RoleType allowedRole : allowedRoles) { - if (allowedRole.getValue() == caller.getType()) { - roleIsAllowed = true; - break; + Object value = lowercaseParams.get(paramName); + if (value == null) { + // for now, assume object array with sub fields + mapArray = new HashMap<Integer, Map>(); + mapValue = new HashMap<String, Object>(); + mapArray.put(Integer.valueOf(index), mapValue); + } else if (value instanceof Map) { + mapArray = (HashMap)value; + mapValue = mapArray.get(Integer.valueOf(index)); + if (mapValue == null) { + mapValue = new HashMap<String, Object>(); + mapArray.put(Integer.valueOf(index), mapValue); } } - } - if (roleIsAllowed) { - validFields.add(field); + // we are ready to store the value for a particular field into the map for this object + mapValue.put(fieldName, params.get(key)); + + lowercaseParams.put(paramName, mapArray); } else { - s_logger.debug("Ignoring paremeter " + parameterAnnotation.name() + " as the caller is not authorized to pass it in"); + lowercaseParams.put(key.toLowerCase(), params.get(key)); } } - - return validFields; + return lowercaseParams; } - protected long getInstanceIdFromJobSuccessResult(final String result) { + protected long getInstanceIdFromJobSuccessResult(String result) { s_logger.debug("getInstanceIdFromJobSuccessResult not overridden in subclass " + this.getClass().getName()); return 0; } - public static boolean isAdmin(final short accountType) { + public static boolean isAdmin(short accountType) { return ((accountType == Account.ACCOUNT_TYPE_ADMIN) || (accountType == Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN) || (accountType == Account.ACCOUNT_TYPE_DOMAIN_ADMIN) || (accountType == Account.ACCOUNT_TYPE_READ_ONLY_ADMIN)); } - public static boolean isRootAdmin(final short accountType) { + public static boolean isRootAdmin(short accountType) { return ((accountType == Account.ACCOUNT_TYPE_ADMIN)); } - public void setFullUrlParams(final Map<String, String> map) { + public void setFullUrlParams(Map<String, String> map) { fullUrlParams = map; } @@ -356,18 +366,18 @@ public abstract class BaseCmd { return fullUrlParams; } - public Long finalyzeAccountId(final String accountName, final Long domainId, final Long projectId, final boolean enabledOnly) { + public Long finalyzeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly) { if (accountName != null) { if (domainId == null) { throw new InvalidParameterValueException("Account must be specified with domainId parameter"); } - final Domain domain = _domainService.getDomain(domainId); + Domain domain = _domainService.getDomain(domainId); if (domain == null) { throw new InvalidParameterValueException("Unable to find domain by id"); } - final Account account = _accountService.getActiveAccountByName(accountName, domainId); + Account account = _accountService.getActiveAccountByName(accountName, domainId); if (account != null && account.getType() != Account.ACCOUNT_TYPE_PROJECT) { if (!enabledOnly || account.getState() == Account.State.enabled) { return account.getId(); @@ -384,12 +394,12 @@ public abstract class BaseCmd { } if (projectId != null) { - final Project project = _projectService.getProject(projectId); + Project project = _projectService.getProject(projectId); if (project != null) { if (!enabledOnly || project.getState() == Project.State.Active) { return project.getProjectAccountId(); } else { - final PermissionDeniedException ex = + PermissionDeniedException ex = new PermissionDeniedException("Can't add resources to the project with specified projectId in state=" + project.getState() + " as it's no longer active"); ex.addProxyObject(project.getUuid(), "projectId"); @@ -401,11 +411,4 @@ public abstract class BaseCmd { } return null; } - - /** - * To be overwritten by any class who needs specific validation - */ - public void validateSpecificParameters(final Map<String, Object> params){ - // To be overwritten by any class who needs specific validation - } } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/782c5306/api/src/org/apache/cloudstack/api/BaseListCmd.java ---------------------------------------------------------------------- diff --git a/api/src/org/apache/cloudstack/api/BaseListCmd.java b/api/src/org/apache/cloudstack/api/BaseListCmd.java index 1876995..c1a4b4c 100644 --- a/api/src/org/apache/cloudstack/api/BaseListCmd.java +++ b/api/src/org/apache/cloudstack/api/BaseListCmd.java @@ -16,10 +16,7 @@ // under the License. package org.apache.cloudstack.api; -import java.util.Map; - import com.cloud.exception.InvalidParameterValueException; -import com.cloud.utils.exception.CSExceptionErrorCode; public abstract class BaseListCmd extends BaseCmd { @@ -86,7 +83,7 @@ public abstract class BaseListCmd extends BaseCmd { public Long getPageSizeVal() { Long defaultPageSize = s_maxPageSize; - final Integer pageSizeInt = getPageSize(); + Integer pageSizeInt = getPageSize(); if (pageSizeInt != null) { defaultPageSize = pageSizeInt.longValue(); } @@ -99,12 +96,12 @@ public abstract class BaseListCmd extends BaseCmd { public Long getStartIndex() { Long startIndex = Long.valueOf(0); - final Long pageSizeVal = getPageSizeVal(); + Long pageSizeVal = getPageSizeVal(); if (pageSizeVal == null) { startIndex = null; } else if (page != null) { - final int pageNum = page.intValue(); + int pageNum = page.intValue(); if (pageNum > 0) { startIndex = Long.valueOf(pageSizeVal * (pageNum - 1)); } @@ -115,25 +112,4 @@ public abstract class BaseListCmd extends BaseCmd { public ApiCommandJobType getInstanceType() { return ApiCommandJobType.None; } - - @Override - public void validateSpecificParameters(final Map<String, Object> params){ - super.validateSpecificParameters(params); - - final Object pageSizeObj = params.get(ApiConstants.PAGE_SIZE); - Long pageSize = null; - if (pageSizeObj != null) { - pageSize = Long.valueOf((String)pageSizeObj); - } - - if (params.get(ApiConstants.PAGE) == null && - pageSize != null && - !pageSize.equals(BaseListCmd.s_pageSizeUnlimited)) { - final ServerApiException ex = new ServerApiException(ApiErrorCode.PARAM_ERROR, "\"page\" parameter is required when \"pagesize\" is specified"); - ex.setCSErrorCode(CSExceptionErrorCode.getCSErrCode(ex.getClass().getName())); - throw ex; - } else if (pageSize == null && (params.get(ApiConstants.PAGE) != null)) { - throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "\"pagesize\" parameter is required when \"page\" is specified"); - } - } } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/782c5306/api/src/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmProfileCmd.java ---------------------------------------------------------------------- diff --git a/api/src/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmProfileCmd.java b/api/src/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmProfileCmd.java index ccdd557..bee1b22 100644 --- a/api/src/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmProfileCmd.java +++ b/api/src/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmProfileCmd.java @@ -151,7 +151,7 @@ public class CreateAutoScaleVmProfileCmd extends BaseAsyncCreateCmd { } Account account = null; if (autoscaleUserId != null) { - final User user = _entityMgr.findById(User.class, autoscaleUserId); + User user = _entityMgr.findById(User.class, autoscaleUserId); account = _entityMgr.findById(Account.class, user.getAccountId()); } else { account = CallContext.current().getCallingAccount(); @@ -167,21 +167,21 @@ public class CreateAutoScaleVmProfileCmd extends BaseAsyncCreateCmd { } if (otherDeployParams == null) return; - final String[] keyValues = otherDeployParams.split("&"); // hostid=123, hypervisor=xenserver - for (final String keyValue : keyValues) { // keyValue == "hostid=123" - final String[] keyAndValue = keyValue.split("="); // keyValue = hostid, 123 + String[] keyValues = otherDeployParams.split("&"); // hostid=123, hypervisor=xenserver + for (String keyValue : keyValues) { // keyValue == "hostid=123" + String[] keyAndValue = keyValue.split("="); // keyValue = hostid, 123 if (keyAndValue.length != 2) { throw new InvalidParameterValueException("Invalid parameter in otherDeployParam : " + keyValue); } - final String paramName = keyAndValue[0]; // hostid - final String paramValue = keyAndValue[1]; // 123 + String paramName = keyAndValue[0]; // hostid + String paramValue = keyAndValue[1]; // 123 otherDeployParamMap.put(paramName, paramValue); } } - public HashMap<String, Object> getDeployParamMap() { + public HashMap<String, String> getDeployParamMap() { createOtherDeployParamMap(); - final HashMap<String, Object> deployParams = new HashMap<String, Object>(otherDeployParamMap); + HashMap<String, String> deployParams = new HashMap<String, String>(otherDeployParamMap); deployParams.put("command", "deployVirtualMachine"); deployParams.put("zoneId", zoneId.toString()); deployParams.put("serviceOfferingId", serviceOfferingId.toString()); @@ -189,7 +189,7 @@ public class CreateAutoScaleVmProfileCmd extends BaseAsyncCreateCmd { return deployParams; } - public String getOtherDeployParam(final String param) { + public String getOtherDeployParam(String param) { if (param == null) { return null; } @@ -232,19 +232,19 @@ public class CreateAutoScaleVmProfileCmd extends BaseAsyncCreateCmd { @Override public void execute() { - final AutoScaleVmProfile result = _entityMgr.findById(AutoScaleVmProfile.class, getEntityId()); - final AutoScaleVmProfileResponse response = _responseGenerator.createAutoScaleVmProfileResponse(result); + AutoScaleVmProfile result = _entityMgr.findById(AutoScaleVmProfile.class, getEntityId()); + AutoScaleVmProfileResponse response = _responseGenerator.createAutoScaleVmProfileResponse(result); response.setResponseName(getCommandName()); - setResponseObject(response); + this.setResponseObject(response); } @Override public void create() throws ResourceAllocationException { - final AutoScaleVmProfile result = _autoScaleService.createAutoScaleVmProfile(this); + AutoScaleVmProfile result = _autoScaleService.createAutoScaleVmProfile(this); if (result != null) { - setEntityId(result.getId()); - setEntityUuid(result.getUuid()); + this.setEntityId(result.getId()); + this.setEntityUuid(result.getUuid()); } else { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create Autoscale Vm Profile"); } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/782c5306/server/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml ---------------------------------------------------------------------- diff --git a/server/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml b/server/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml index f35ee5e..fd2f5fb 100644 --- a/server/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml +++ b/server/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml @@ -50,16 +50,6 @@ <bean id="apiDispatcher" class="com.cloud.api.ApiDispatcher" /> - <bean id="dispatchChainFactory" class="com.cloud.api.dispatch.DispatchChainFactory" /> - - <bean id="paramSemanticValidationWorker" class="com.cloud.api.dispatch.ParamSemanticValidationWorker" /> - - <bean id="paramProcessWorker" class="com.cloud.api.dispatch.ParamProcessWorker" /> - - <bean id="paramGenericValidationWorker" class="com.cloud.api.dispatch.ParamGenericValidationWorker" /> - - <bean id="commandCreationWorker" class="com.cloud.api.dispatch.CommandCreationWorker" /> - <bean id="apiResponseHelper" class="com.cloud.api.ApiResponseHelper" /> <bean id="apiServer" class="com.cloud.api.ApiServer"> http://git-wip-us.apache.org/repos/asf/cloudstack/blob/782c5306/server/src/com/cloud/api/ApiAsyncJobDispatcher.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/api/ApiAsyncJobDispatcher.java b/server/src/com/cloud/api/ApiAsyncJobDispatcher.java index 565e3a4..71ac616 100644 --- a/server/src/com/cloud/api/ApiAsyncJobDispatcher.java +++ b/server/src/com/cloud/api/ApiAsyncJobDispatcher.java @@ -70,30 +70,30 @@ public class ApiAsyncJobDispatcher extends AdapterBase implements AsyncJobDispat }); } - protected void runJobInContext(final AsyncJob job) { + protected void runJobInContext(AsyncJob job) { BaseAsyncCmd cmdObj = null; try { - final Class<?> cmdClass = Class.forName(job.getCmd()); + Class<?> cmdClass = Class.forName(job.getCmd()); cmdObj = (BaseAsyncCmd)cmdClass.newInstance(); cmdObj = ComponentContext.inject(cmdObj); cmdObj.configure(); cmdObj.setJob(job); - final Type mapType = new TypeToken<Map<String, String>>() { + Type mapType = new TypeToken<Map<String, String>>() { }.getType(); - final Gson gson = ApiGsonHelper.getBuilder().create(); - final Map<String, Object> params = gson.fromJson(job.getCmdInfo(), mapType); + Gson gson = ApiGsonHelper.getBuilder().create(); + Map<String, String> params = gson.fromJson(job.getCmdInfo(), mapType); // whenever we deserialize, the UserContext needs to be updated - final String userIdStr = (String) params.get("ctxUserId"); - final String acctIdStr = (String) params.get("ctxAccountId"); + String userIdStr = params.get("ctxUserId"); + String acctIdStr = params.get("ctxAccountId"); Long userId = null; Account accountObject = null; if (cmdObj instanceof BaseAsyncCreateCmd) { - final BaseAsyncCreateCmd create = (BaseAsyncCreateCmd)cmdObj; - create.setEntityId(Long.parseLong((String) params.get("id"))); - create.setEntityUuid((String) params.get("uuid")); + BaseAsyncCreateCmd create = (BaseAsyncCreateCmd)cmdObj; + create.setEntityId(Long.parseLong(params.get("id"))); + create.setEntityUuid(params.get("uuid")); } User user = null; @@ -116,19 +116,19 @@ public class ApiAsyncJobDispatcher extends AdapterBase implements AsyncJobDispat } finally { CallContext.unregister(); } - } catch (final Throwable e) { + } catch (Throwable e) { String errorMsg = null; int errorCode = ApiErrorCode.INTERNAL_ERROR.getHttpCode(); if (!(e instanceof ServerApiException)) { s_logger.error("Unexpected exception while executing " + job.getCmd(), e); errorMsg = e.getMessage(); } else { - final ServerApiException sApiEx = (ServerApiException)e; + ServerApiException sApiEx = (ServerApiException)e; errorMsg = sApiEx.getDescription(); errorCode = sApiEx.getErrorCode().getHttpCode(); } - final ExceptionResponse response = new ExceptionResponse(); + ExceptionResponse response = new ExceptionResponse(); response.setErrorCode(errorCode); response.setErrorText(errorMsg); response.setResponseName((cmdObj == null) ? "unknowncommandresponse" : cmdObj.getCommandName()); http://git-wip-us.apache.org/repos/asf/cloudstack/blob/782c5306/server/src/com/cloud/api/ApiDispatcher.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/api/ApiDispatcher.java b/server/src/com/cloud/api/ApiDispatcher.java index 7d9b7d7..ed95c72 100755 --- a/server/src/com/cloud/api/ApiDispatcher.java +++ b/server/src/com/cloud/api/ApiDispatcher.java @@ -16,67 +16,126 @@ // under the License. package com.cloud.api; +import static org.apache.commons.lang.StringUtils.isNotBlank; + +import java.lang.reflect.Field; +import java.text.DateFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.StringTokenizer; +import java.util.regex.Matcher; import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.acl.InfrastructureEntity; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.BaseAsyncCreateCmd; import org.apache.cloudstack.api.BaseAsyncCustomIdCmd; import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.BaseCmd.CommandType; import org.apache.cloudstack.api.BaseCustomIdCmd; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.resource.ArchiveAlertsCmd; +import org.apache.cloudstack.api.command.admin.resource.DeleteAlertsCmd; +import org.apache.cloudstack.api.command.user.event.ArchiveEventsCmd; +import org.apache.cloudstack.api.command.user.event.DeleteEventsCmd; +import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.jobs.AsyncJob; import org.apache.cloudstack.framework.jobs.AsyncJobManager; import org.apache.log4j.Logger; -import com.cloud.api.dispatch.DispatchChain; -import com.cloud.api.dispatch.DispatchChainFactory; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.utils.DateUtil; +import com.cloud.utils.ReflectUtil; +import com.cloud.utils.db.EntityManager; +import com.cloud.utils.exception.CSExceptionErrorCode; +import com.cloud.utils.exception.CloudRuntimeException; public class ApiDispatcher { private static final Logger s_logger = Logger.getLogger(ApiDispatcher.class.getName()); Long _createSnapshotQueueSizeLimit; - @Inject AsyncJobManager _asyncMgr = null; + @Inject + AccountManager _accountMgr = null; + @Inject + EntityManager _entityMgr = null; - @Inject() - protected DispatchChainFactory dispatchChainFactory = null; - - protected DispatchChain standardDispatchChain = null; + private static ApiDispatcher s_instance; - protected DispatchChain asyncCreationDispatchChain = null; + public static ApiDispatcher getInstance() { + return s_instance; + } public ApiDispatcher() { } @PostConstruct - public void setup() { - standardDispatchChain = dispatchChainFactory.getStandardDispatchChain(); - asyncCreationDispatchChain = dispatchChainFactory.getAsyncCreationDispatchChain(); + void init() { + s_instance = this; } - public void setCreateSnapshotQueueSizeLimit(final Long snapshotLimit) { + public void setCreateSnapshotQueueSizeLimit(Long snapshotLimit) { _createSnapshotQueueSizeLimit = snapshotLimit; } - public void dispatchCreateCmd(final BaseAsyncCreateCmd cmd, final Map<String, Object> params) throws Exception { - asyncCreationDispatchChain.dispatch(cmd, params); + public void dispatchCreateCmd(BaseAsyncCreateCmd cmd, Map<String, String> params) throws Exception { + processParameters(cmd, params); + + cmd.create(); + } - public void dispatch(final BaseCmd cmd, final Map<String, Object> params, final boolean execute) throws Exception { - // Let the chain of responsibility dispatch gradually - standardDispatchChain.dispatch(cmd, params); + private void doAccessChecks(BaseCmd cmd, Map<Object, AccessType> entitiesToAccess) { + Account caller = CallContext.current().getCallingAccount(); + Account owner = _accountMgr.getActiveAccountById(cmd.getEntityOwnerId()); - final CallContext ctx = CallContext.current(); + if (cmd instanceof BaseAsyncCreateCmd) { + //check that caller can access the owner account. + _accountMgr.checkAccess(caller, null, true, owner); + } + + if (!entitiesToAccess.isEmpty()) { + //check that caller can access the owner account. + _accountMgr.checkAccess(caller, null, true, owner); + for (Object entity : entitiesToAccess.keySet()) { + if (entity instanceof ControlledEntity) { + _accountMgr.checkAccess(caller, entitiesToAccess.get(entity), true, (ControlledEntity)entity); + } else if (entity instanceof InfrastructureEntity) { + //FIXME: Move this code in adapter, remove code from Account manager + } + } + } + } + + public void dispatch(BaseCmd cmd, Map<String, String> params, boolean execute) throws Exception { + processParameters(cmd, params); + CallContext ctx = CallContext.current(); if (cmd instanceof BaseAsyncCmd) { - final BaseAsyncCmd asyncCmd = (BaseAsyncCmd)cmd; - final String startEventId = (String) params.get(ApiConstants.CTX_START_EVENT_ID); + BaseAsyncCmd asyncCmd = (BaseAsyncCmd)cmd; + String startEventId = params.get("ctxStartEventId"); ctx.setStartEventId(Long.valueOf(startEventId)); // Synchronise job on the object if needed @@ -107,6 +166,392 @@ public class ApiDispatcher { } cmd.execute(); + } + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void processParameters(BaseCmd cmd, Map<String, String> params) { + Map<Object, AccessType> entitiesToAccess = new HashMap<Object, AccessType>(); + Map<String, Object> unpackedParams = cmd.unpackParams(params); + + if (cmd instanceof BaseListCmd) { + Object pageSizeObj = unpackedParams.get(ApiConstants.PAGE_SIZE); + Long pageSize = null; + if (pageSizeObj != null) { + pageSize = Long.valueOf((String)pageSizeObj); + } + + if ((unpackedParams.get(ApiConstants.PAGE) == null) && (pageSize != null && !pageSize.equals(BaseListCmd.s_pageSizeUnlimited))) { + ServerApiException ex = new ServerApiException(ApiErrorCode.PARAM_ERROR, "\"page\" parameter is required when \"pagesize\" is specified"); + ex.setCSErrorCode(CSExceptionErrorCode.getCSErrCode(ex.getClass().getName())); + throw ex; + } else if (pageSize == null && (unpackedParams.get(ApiConstants.PAGE) != null)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "\"pagesize\" parameter is required when \"page\" is specified"); + } + } + + List<Field> fields = ReflectUtil.getAllFieldsForClass(cmd.getClass(), BaseCmd.class); + + for (Field field : fields) { + Parameter parameterAnnotation = field.getAnnotation(Parameter.class); + if ((parameterAnnotation == null) || !parameterAnnotation.expose()) { + continue; + } + + //TODO: Annotate @Validate on API Cmd classes, FIXME how to process Validate + RoleType[] allowedRoles = parameterAnnotation.authorized(); + if (allowedRoles.length > 0) { + boolean permittedParameter = false; + Account caller = CallContext.current().getCallingAccount(); + for (RoleType allowedRole : allowedRoles) { + if (allowedRole.getValue() == caller.getType()) { + permittedParameter = true; + break; + } + } + if (!permittedParameter) { + s_logger.debug("Ignoring paremeter " + parameterAnnotation.name() + " as the caller is not authorized to pass it in"); + continue; + } + } + + Object paramObj = unpackedParams.get(parameterAnnotation.name()); + if (paramObj == null) { + if (parameterAnnotation.required()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to execute API command " + + cmd.getCommandName().substring(0, cmd.getCommandName().length() - 8) + " due to missing parameter " + parameterAnnotation.name()); + } + continue; + } + + // marshall the parameter into the correct type and set the field value + try { + setFieldValue(field, cmd, paramObj, parameterAnnotation); + } catch (IllegalArgumentException argEx) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Unable to execute API command " + cmd.getCommandName() + " due to invalid value " + paramObj + " for parameter " + + parameterAnnotation.name()); + } + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to execute API command " + + cmd.getCommandName().substring(0, cmd.getCommandName().length() - 8) + " due to invalid value " + paramObj + " for parameter " + + parameterAnnotation.name()); + } catch (ParseException parseEx) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Invalid date parameter " + paramObj + " passed to command " + cmd.getCommandName().substring(0, cmd.getCommandName().length() - 8)); + } + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to parse date " + paramObj + " for command " + + cmd.getCommandName().substring(0, cmd.getCommandName().length() - 8) + ", please pass dates in the format mentioned in the api documentation"); + } catch (InvalidParameterValueException invEx) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to execute API command " + + cmd.getCommandName().substring(0, cmd.getCommandName().length() - 8) + " due to invalid value. " + invEx.getMessage()); + } catch (CloudRuntimeException cloudEx) { + s_logger.error("CloudRuntimeException", cloudEx); + // FIXME: Better error message? This only happens if the API command is not executable, which typically + //means + // there was + // and IllegalAccessException setting one of the parameters. + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Internal error executing API command " + + cmd.getCommandName().substring(0, cmd.getCommandName().length() - 8)); + } + + //check access on the resource this field points to + try { + ACL checkAccess = field.getAnnotation(ACL.class); + CommandType fieldType = parameterAnnotation.type(); + + if (checkAccess != null) { + // Verify that caller can perform actions in behalf of vm owner + //acumulate all Controlled Entities together. + + //parse the array of resource types and in case of map check access on key or value or both as specified in @acl + //implement external dao for classes that need findByName + //for maps, specify access to be checkd on key or value. + + // find the controlled entity DBid by uuid + if (parameterAnnotation.entityType() != null) { + Class<?>[] entityList = parameterAnnotation.entityType()[0].getAnnotation(EntityReference.class).value(); + + for (Class entity : entityList) { + // Check if the parameter type is a single + // Id or list of id's/name's + switch (fieldType) { + case LIST: + CommandType listType = parameterAnnotation.collectionType(); + switch (listType) { + case LONG: + case UUID: + List<Long> listParam = (List<Long>)field.get(cmd); + for (Long entityId : listParam) { + Object entityObj = s_instance._entityMgr.findById(entity, entityId); + entitiesToAccess.put(entityObj, checkAccess.accessType()); + } + break; + /* + * case STRING: List<String> listParam = + * new ArrayList<String>(); listParam = + * (List)field.get(cmd); for(String + * entityName: listParam){ + * ControlledEntity entityObj = + * (ControlledEntity + * )daoClassInstance(entityId); + * entitiesToAccess.add(entityObj); } + * break; + */ + default: + break; + } + break; + case LONG: + case UUID: + Object entityObj = s_instance._entityMgr.findById(entity, (Long)field.get(cmd)); + entitiesToAccess.put(entityObj, checkAccess.accessType()); + break; + default: + break; + } + + if (ControlledEntity.class.isAssignableFrom(entity)) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("ControlledEntity name is:" + entity.getName()); + } + } + + if (InfrastructureEntity.class.isAssignableFrom(entity)) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("InfrastructureEntity name is:" + entity.getName()); + } + } + } + + } + + } + + } catch (IllegalArgumentException e) { + s_logger.error("Error initializing command " + cmd.getCommandName() + ", field " + field.getName() + " is not accessible."); + throw new CloudRuntimeException("Internal error initializing parameters for command " + cmd.getCommandName() + " [field " + field.getName() + + " is not accessible]"); + } catch (IllegalAccessException e) { + s_logger.error("Error initializing command " + cmd.getCommandName() + ", field " + field.getName() + " is not accessible."); + throw new CloudRuntimeException("Internal error initializing parameters for command " + cmd.getCommandName() + " [field " + field.getName() + + " is not accessible]"); + } + + } + + //check access on the entities. + getInstance().doAccessChecks(cmd, entitiesToAccess); + + } + + private static Long translateUuidToInternalId(String uuid, Parameter annotation) { + if (uuid.equals("-1")) { + // FIXME: This is to handle a lot of hardcoded special cases where -1 is sent + // APITODO: Find and get rid of all hardcoded params in API Cmds and service layer + return -1L; + } + Long internalId = null; + // If annotation's empty, the cmd existed before 3.x try conversion to long + boolean isPre3x = annotation.since().isEmpty(); + // Match against Java's UUID regex to check if input is uuid string + boolean isUuid = uuid.matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"); + // Enforce that it's uuid for newly added apis from version 3.x + if (!isPre3x && !isUuid) + return null; + // Allow both uuid and internal id for pre3x apis + if (isPre3x && !isUuid) { + try { + internalId = Long.parseLong(uuid); + } catch (NumberFormatException e) { + internalId = null; + } + if (internalId != null) + return internalId; + } + // There may be multiple entities defined on the @EntityReference of a Response.class + // UUID CommandType would expect only one entityType, so use the first entityType + Class<?>[] entities = annotation.entityType()[0].getAnnotation(EntityReference.class).value(); + // Go through each entity which is an interface to a VO class and get a VO object + // Try to getId() for the object using reflection, break on first non-null value + for (Class<?> entity : entities) { + // For backward compatibility, we search within removed entities and let service layer deal + // with removed ones, return empty response or error + Object objVO = s_instance._entityMgr.findByUuidIncludingRemoved(entity, uuid); + if (objVO == null) { + continue; + } + // Invoke the getId method, get the internal long ID + // If that fails hide exceptions as the uuid may not exist + try { + internalId = ((InternalIdentity)objVO).getId(); + } catch (IllegalArgumentException e) { + } catch (NullPointerException e) { + } + // Return on first non-null Id for the uuid entity + if (internalId != null) + break; + } + if (internalId == null) { + if (s_logger.isDebugEnabled()) + s_logger.debug("Object entity uuid = " + uuid + " does not exist in the database."); + throw new InvalidParameterValueException("Invalid parameter " + annotation.name() + " value=" + uuid + + " due to incorrect long value format, or entity does not exist or due to incorrect parameter annotation for the field in api cmd class."); + } + return internalId; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void setFieldValue(Field field, BaseCmd cmdObj, Object paramObj, Parameter annotation) throws IllegalArgumentException, ParseException { + try { + field.setAccessible(true); + CommandType fieldType = annotation.type(); + switch (fieldType) { + case BOOLEAN: + field.set(cmdObj, Boolean.valueOf(paramObj.toString())); + break; + case DATE: + // This piece of code is for maintaining backward compatibility + // and support both the date formats(Bug 9724) + // Do the date messaging for ListEventsCmd only + if (cmdObj instanceof ListEventsCmd || cmdObj instanceof DeleteEventsCmd || cmdObj instanceof ArchiveEventsCmd || + cmdObj instanceof ArchiveAlertsCmd || cmdObj instanceof DeleteAlertsCmd) { + boolean isObjInNewDateFormat = isObjInNewDateFormat(paramObj.toString()); + if (isObjInNewDateFormat) { + DateFormat newFormat = BaseCmd.NEW_INPUT_FORMAT; + synchronized (newFormat) { + field.set(cmdObj, newFormat.parse(paramObj.toString())); + } + } else { + DateFormat format = BaseCmd.INPUT_FORMAT; + synchronized (format) { + Date date = format.parse(paramObj.toString()); + if (field.getName().equals("startDate")) { + date = messageDate(date, 0, 0, 0); + } else if (field.getName().equals("endDate")) { + date = messageDate(date, 23, 59, 59); + } + field.set(cmdObj, date); + } + } + } else { + DateFormat format = BaseCmd.INPUT_FORMAT; + synchronized (format) { + format.setLenient(false); + field.set(cmdObj, format.parse(paramObj.toString())); + } + } + break; + case FLOAT: + // Assuming that the parameters have been checked for required before now, + // we ignore blank or null values and defer to the command to set a default + // value for optional parameters ... + if (paramObj != null && isNotBlank(paramObj.toString())) { + field.set(cmdObj, Float.valueOf(paramObj.toString())); + } + break; + case INTEGER: + // Assuming that the parameters have been checked for required before now, + // we ignore blank or null values and defer to the command to set a default + // value for optional parameters ... + if (paramObj != null && isNotBlank(paramObj.toString())) { + field.set(cmdObj, Integer.valueOf(paramObj.toString())); + } + break; + case LIST: + List listParam = new ArrayList(); + StringTokenizer st = new StringTokenizer(paramObj.toString(), ","); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + CommandType listType = annotation.collectionType(); + switch (listType) { + case INTEGER: + listParam.add(Integer.valueOf(token)); + break; + case UUID: + if (token.isEmpty()) + break; + Long internalId = translateUuidToInternalId(token, annotation); + listParam.add(internalId); + break; + case LONG: { + listParam.add(Long.valueOf(token)); + } + break; + case SHORT: + listParam.add(Short.valueOf(token)); + case STRING: + listParam.add(token); + break; + } + } + field.set(cmdObj, listParam); + break; + case UUID: + if (paramObj.toString().isEmpty()) + break; + Long internalId = translateUuidToInternalId(paramObj.toString(), annotation); + field.set(cmdObj, internalId); + break; + case LONG: + field.set(cmdObj, Long.valueOf(paramObj.toString())); + break; + case SHORT: + field.set(cmdObj, Short.valueOf(paramObj.toString())); + break; + case STRING: + if ((paramObj != null) && paramObj.toString().length() > annotation.length()) { + s_logger.error("Value greater than max allowed length " + annotation.length() + " for param: " + field.getName()); + throw new InvalidParameterValueException("Value greater than max allowed length " + annotation.length() + " for param: " + field.getName()); + } + field.set(cmdObj, paramObj.toString()); + break; + case TZDATE: + field.set(cmdObj, DateUtil.parseTZDateString(paramObj.toString())); + break; + case MAP: + default: + field.set(cmdObj, paramObj); + break; + } + } catch (IllegalAccessException ex) { + s_logger.error("Error initializing command " + cmdObj.getCommandName() + ", field " + field.getName() + " is not accessible."); + throw new CloudRuntimeException("Internal error initializing parameters for command " + cmdObj.getCommandName() + " [field " + field.getName() + + " is not accessible]"); + } + } + + private static boolean isObjInNewDateFormat(String string) { + Matcher matcher = BaseCmd.newInputDateFormat.matcher(string); + return matcher.matches(); + } + + private static Date messageDate(Date date, int hourOfDay, int minute, int second) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.set(Calendar.HOUR_OF_DAY, hourOfDay); + cal.set(Calendar.MINUTE, minute); + cal.set(Calendar.SECOND, second); + return cal.getTime(); + } + + public static void plugService(Field field, BaseCmd cmd) { + + Class<?> fc = field.getType(); + Object instance = null; + + if (instance == null) { + throw new CloudRuntimeException("Unable to plug service " + fc.getSimpleName() + " in command " + cmd.getClass().getSimpleName()); + } + + try { + field.setAccessible(true); + field.set(cmd, instance); + } catch (IllegalArgumentException e) { + s_logger.error("IllegalArgumentException at plugService for command " + cmd.getCommandName() + ", field " + field.getName()); + throw new CloudRuntimeException("Internal error at plugService for command " + cmd.getCommandName() + " [Illegal argumet at field " + field.getName() + "]"); + } catch (IllegalAccessException e) { + s_logger.error("Error at plugService for command " + cmd.getCommandName() + ", field " + field.getName() + " is not accessible."); + throw new CloudRuntimeException("Internal error at plugService for command " + cmd.getCommandName() + " [field " + field.getName() + " is not accessible]"); + } + } }