You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ml...@apache.org on 2014/03/08 01:23:14 UTC

[41/50] [abbrv] Dispatcher corrections, refactoring and tests

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/c211f0bb/server/src/com/cloud/api/dispatch/ParamGenericValidationWorker.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/api/dispatch/ParamGenericValidationWorker.java b/server/src/com/cloud/api/dispatch/ParamGenericValidationWorker.java
new file mode 100644
index 0000000..0c829e8
--- /dev/null
+++ b/server/src/com/cloud/api/dispatch/ParamGenericValidationWorker.java
@@ -0,0 +1,103 @@
+// 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 com.cloud.api.dispatch;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.log4j.Logger;
+
+/**
+ * This worker validates parameters in a generic way, by using annotated
+ * restrictions without involving the {@Link BaseCmd}. This worker doesn't
+ * know or care about the meaning of the parameters and that's why we can
+ * have it out of the {@Link BaseCmd}
+ *
+ * @author afornie
+ */
+public class ParamGenericValidationWorker implements DispatchWorker {
+
+    protected static Logger s_logger = Logger.getLogger(ParamGenericValidationWorker.class.getName());
+
+    protected static List<String> defaultParamNames = new ArrayList<String>();
+
+    static {
+        defaultParamNames.add(ApiConstants.CTX_START_EVENT_ID);
+        defaultParamNames.add(ApiConstants.COMMAND);
+        defaultParamNames.add(ApiConstants.CMD_EVENT_TYPE);
+        defaultParamNames.add(ApiConstants.USERNAME);
+        defaultParamNames.add(ApiConstants.USER_ID);
+        defaultParamNames.add(ApiConstants.PASSWORD);
+        defaultParamNames.add(ApiConstants.DOMAIN);
+        defaultParamNames.add(ApiConstants.DOMAIN_ID);
+        defaultParamNames.add(ApiConstants.DOMAIN__ID);
+        defaultParamNames.add(ApiConstants.SESSIONKEY);
+        defaultParamNames.add(ApiConstants.RESPONSE);
+        defaultParamNames.add(ApiConstants.PAGE);
+        defaultParamNames.add(ApiConstants.USER_API_KEY);
+        defaultParamNames.add(ApiConstants.API_KEY);
+        defaultParamNames.add(ApiConstants.PAGE_SIZE);
+        defaultParamNames.add(ApiConstants.HTTPMETHOD);
+        defaultParamNames.add(ApiConstants.SIGNATURE);
+        defaultParamNames.add(ApiConstants.CTX_ACCOUNT_ID);
+        defaultParamNames.add(ApiConstants.CTX_START_EVENT_ID);
+        defaultParamNames.add(ApiConstants.CTX_USER_ID);
+        defaultParamNames.add("_");
+    }
+
+    protected static final String ERROR_MSG_PREFIX = "Unknown parameters :";
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public void handle(final DispatchTask task) {
+        final BaseCmd cmd = task.getCmd();
+        final Map params = task.getParams();
+
+        final List<String> expectedParamNames = getParamNamesForCommand(cmd);
+
+        final StringBuilder errorMsg = new StringBuilder(ERROR_MSG_PREFIX);
+        boolean foundUnknownParam = false;
+        for (final Object paramName : params.keySet()) {
+            if (!expectedParamNames.contains(paramName)) {
+                errorMsg.append(" ").append(paramName);
+                foundUnknownParam= true;
+            }
+        }
+
+        if (foundUnknownParam) {
+            s_logger.warn(String.format("Received unknown parameters for command %s. %s", cmd.getActualCommandName(), errorMsg));
+        }
+    }
+
+    protected List<String> getParamNamesForCommand(final BaseCmd cmd) {
+        final List<String> paramNames = new ArrayList<String>();
+        // The expected param names are all the specific for the current command class ...
+        for (final Field field : cmd.getParamFields()) {
+            final Parameter parameterAnnotation = field.getAnnotation(Parameter.class);
+            paramNames.add(parameterAnnotation.name());
+        }
+        // ... plus the default ones
+        paramNames.addAll(defaultParamNames);
+        return paramNames;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/c211f0bb/server/src/com/cloud/api/dispatch/ParamProcessWorker.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/api/dispatch/ParamProcessWorker.java b/server/src/com/cloud/api/dispatch/ParamProcessWorker.java
new file mode 100644
index 0000000..e9bdd8b
--- /dev/null
+++ b/server/src/com/cloud/api/dispatch/ParamProcessWorker.java
@@ -0,0 +1,428 @@
+// 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 com.cloud.api.dispatch;
+
+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.inject.Inject;
+
+import org.apache.log4j.Logger;
+
+import org.apache.cloudstack.acl.ControlledEntity;
+import org.apache.cloudstack.acl.InfrastructureEntity;
+import org.apache.cloudstack.acl.SecurityChecker.AccessType;
+import org.apache.cloudstack.api.ACL;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseAsyncCreateCmd;
+import org.apache.cloudstack.api.BaseCmd;
+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.BaseCmd.CommandType;
+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 com.cloud.exception.InvalidParameterValueException;
+import com.cloud.user.Account;
+import com.cloud.user.AccountManager;
+import com.cloud.utils.DateUtil;
+import com.cloud.utils.db.EntityManager;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+public class ParamProcessWorker implements DispatchWorker {
+
+    private static final Logger s_logger = Logger.getLogger(ParamProcessWorker.class.getName());
+
+    @Inject
+    protected AccountManager _accountMgr;
+
+    @Inject
+    protected EntityManager _entityMgr;
+
+    @Override
+    public void handle(final DispatchTask task) {
+        processParameters(task.getCmd(), task.getParams());
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public void processParameters(final BaseCmd cmd, final Map params) {
+        final Map<Object, AccessType> entitiesToAccess = new HashMap<Object, AccessType>();
+
+        final List<Field> cmdFields = cmd.getParamFields();
+
+        for (final Field field : cmdFields) {
+            final Parameter parameterAnnotation = field.getAnnotation(Parameter.class);
+            final Object paramObj = params.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 (final 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 (final 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 (final 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 (final 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 {
+                final ACL checkAccess = field.getAnnotation(ACL.class);
+                final 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) {
+                        final Class<?>[] entityList = parameterAnnotation.entityType()[0].getAnnotation(EntityReference.class).value();
+
+                        for (final Class entity : entityList) {
+                            // Check if the parameter type is a single
+                            // Id or list of id's/name's
+                            switch (fieldType) {
+                                case LIST:
+                                    final CommandType listType = parameterAnnotation.collectionType();
+                                    switch (listType) {
+                                        case LONG:
+                                        case UUID:
+                                            final List<Long> listParam = (List<Long>)field.get(cmd);
+                                            for (final Long entityId : listParam) {
+                                                final Object entityObj = _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:
+                                    final Object entityObj = _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 (final 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 (final 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]");
+            }
+
+        }
+
+        doAccessChecks(cmd, entitiesToAccess);
+    }
+
+
+    private void doAccessChecks(final BaseCmd cmd, final Map<Object, AccessType> entitiesToAccess) {
+        final Account caller = CallContext.current().getCallingAccount();
+        final Account owner = _accountMgr.getActiveAccountById(cmd.getEntityOwnerId());
+
+        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 (final 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
+                }
+            }
+        }
+    }
+
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private void setFieldValue(final Field field, final BaseCmd cmdObj, final Object paramObj, final Parameter annotation) throws IllegalArgumentException, ParseException {
+        try {
+            field.setAccessible(true);
+            final 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) {
+                        final boolean isObjInNewDateFormat = isObjInNewDateFormat(paramObj.toString());
+                        if (isObjInNewDateFormat) {
+                            final DateFormat newFormat = BaseCmd.NEW_INPUT_FORMAT;
+                            synchronized (newFormat) {
+                                field.set(cmdObj, newFormat.parse(paramObj.toString()));
+                            }
+                        } else {
+                            final 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 {
+                        final 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:
+                    final List listParam = new ArrayList();
+                    final StringTokenizer st = new StringTokenizer(paramObj.toString(), ",");
+                    while (st.hasMoreTokens()) {
+                        final String token = st.nextToken();
+                        final CommandType listType = annotation.collectionType();
+                        switch (listType) {
+                            case INTEGER:
+                                listParam.add(Integer.valueOf(token));
+                                break;
+                            case UUID:
+                                if (token.isEmpty())
+                                    break;
+                                final 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;
+                    final 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 (final 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 boolean isObjInNewDateFormat(final String string) {
+        final Matcher matcher = BaseCmd.newInputDateFormat.matcher(string);
+        return matcher.matches();
+    }
+
+    private Date messageDate(final Date date, final int hourOfDay, final int minute, final int second) {
+        final 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();
+    }
+
+    private Long translateUuidToInternalId(final String uuid, final 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
+        final boolean isPre3x = annotation.since().isEmpty();
+        // Match against Java's UUID regex to check if input is uuid string
+        final 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 (final 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
+        final 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 (final Class<?> entity : entities) {
+            // For backward compatibility, we search within removed entities and let service layer deal
+            // with removed ones, return empty response or error
+            final Object objVO = _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 (final IllegalArgumentException e) {
+            } catch (final 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;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/c211f0bb/server/src/com/cloud/api/dispatch/ParamUnpackWorker.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/api/dispatch/ParamUnpackWorker.java b/server/src/com/cloud/api/dispatch/ParamUnpackWorker.java
new file mode 100644
index 0000000..12e1226
--- /dev/null
+++ b/server/src/com/cloud/api/dispatch/ParamUnpackWorker.java
@@ -0,0 +1,114 @@
+// 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 com.cloud.api.dispatch;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.ServerApiException;
+
+public class ParamUnpackWorker implements DispatchWorker {
+
+    private static final Logger s_logger = Logger.getLogger(ParamUnpackWorker.class);
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    @Override
+    public void handle(final DispatchTask task) throws ServerApiException {
+        final Map<String, Object> lowercaseParams = new HashMap<String, Object>();
+        final Map<String, String> params = task.getParams();
+        for (final String key : params.keySet()) {
+            final int arrayStartIndex = key.indexOf('[');
+            final 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 (arrayStartIndex > 0) {
+                final int arrayEndIndex = key.indexOf(']');
+                final 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");
+                }
+
+                // Now that we have an array object, check for a field name in the case of a complex object
+                final 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);
+                }
+
+                // 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;
+                final String indexStr = key.substring(arrayStartIndex + 1, arrayEndIndex);
+                int index = 0;
+                boolean parsedIndex = false;
+                try {
+                    if (indexStr != null) {
+                        index = Integer.parseInt(indexStr);
+                        parsedIndex = true;
+                    }
+                } catch (final NumberFormatException nfe) {
+                    s_logger.warn("Invalid parameter " + key + " received, unable to parse object array, returning an error.");
+                }
+
+                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");
+                }
+
+                final 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);
+                    }
+                }
+
+                // 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 {
+                lowercaseParams.put(key.toLowerCase(), params.get(key));
+            }
+        }
+
+        // The chain continues processing the unpacked parameters
+        task.setParams(lowercaseParams);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/c211f0bb/server/src/com/cloud/api/dispatch/SpecificCmdValidationWorker.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/api/dispatch/SpecificCmdValidationWorker.java b/server/src/com/cloud/api/dispatch/SpecificCmdValidationWorker.java
new file mode 100644
index 0000000..3566a1a
--- /dev/null
+++ b/server/src/com/cloud/api/dispatch/SpecificCmdValidationWorker.java
@@ -0,0 +1,34 @@
+// 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 com.cloud.api.dispatch;
+
+/**
+ * This worker validates parameters in a semantic way, that is of
+ * course specific for each {@link BaseCmd}, so actually it delegates
+ * the validation on the {@link BaseCmd} itself
+ *
+ */
+public class SpecificCmdValidationWorker implements DispatchWorker {
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void handle(final DispatchTask task) {
+        task.getCmd().validateSpecificParameters(task.getParams());
+    }
+
+}