From 2c04dad529ff9b67f78d86316a9e1f6e93d503a4 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Tue, 8 Jan 2013 18:48:18 -0800 Subject: [PATCH] ApiDiscovery: New plugin to help discover apis on mgmt server - Introduces api/discovery plugin that helps discover apis on the mgmt server - It's a pluggable service, therefore has it's own api-discovery_commands.properties where the discovery api, listApi can be blacklisted (by removing it), or it's role mask can be changed - By default its response has all the apis - Changes in other parts of the code to make it work, viz. components.xml, pom.xml, and in ApiServer where it is used as an adapter to get apiname, cmd mappings The ApiDiscoveryService interface is a contract that the implementing class will provide: 1. A means to get all the apis as a list of response, plugin is free to implement the response class, as long as it extends on the BaseResponse: ListResponse listApis(); 2. Provides a map of apiname as the key and cmd class as the value: Map> getApiNameCmdClassMapping(); Signed-off-by: Rohit Yadav --- .../apache/cloudstack/api/ApiConstants.java | 5 + .../discovery/ApiDiscoveryService.java | 29 ++++ client/pom.xml | 5 + .../api-discovery_commands.properties.in | 23 +++ client/tomcatconf/components.xml.in | 4 + plugins/api/discovery/pom.xml | 44 ++++++ .../command/user/discovery/ListApisCmd.java | 55 +++++++ .../api/response/ApiDiscoveryResponse.java | 75 +++++++++ .../api/response/ApiParameterResponse.java | 70 +++++++++ .../discovery/ApiDiscoveryServiceImpl.java | 144 ++++++++++++++++++ plugins/pom.xml | 1 + server/src/com/cloud/api/ApiServer.java | 18 +-- 12 files changed, 462 insertions(+), 11 deletions(-) create mode 100644 api/src/org/apache/cloudstack/discovery/ApiDiscoveryService.java create mode 100644 client/tomcatconf/api-discovery_commands.properties.in create mode 100644 plugins/api/discovery/pom.xml create mode 100644 plugins/api/discovery/src/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java create mode 100644 plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiDiscoveryResponse.java create mode 100644 plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiParameterResponse.java create mode 100644 plugins/api/discovery/src/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 6d8400ae6fd..b4ce24c2bc9 100644 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -91,6 +91,7 @@ public class ApiConstants { public static final String INTERVAL_TYPE = "intervaltype"; public static final String IP_ADDRESS = "ipaddress"; public static final String IP_ADDRESS_ID = "ipaddressid"; + public static final String IS_ASYNC = "isasync"; public static final String IP_AVAILABLE = "ipavailable"; public static final String IP_LIMIT = "iplimit"; public static final String IP_TOTAL = "iptotal"; @@ -106,6 +107,7 @@ public class ApiConstants { public static final String JOB_STATUS = "jobstatus"; public static final String LASTNAME = "lastname"; public static final String LEVEL = "level"; + public static final String LENGTH = "length"; public static final String LIMIT_CPU_USE = "limitcpuuse"; public static final String LOCK = "lock"; public static final String LUN = "lun"; @@ -126,6 +128,7 @@ public class ApiConstants { public static final String OP = "op"; public static final String OS_CATEGORY_ID = "oscategoryid"; public static final String OS_TYPE_ID = "ostypeid"; + public static final String PARAMS = "params"; public static final String PARENT_DOMAIN_ID = "parentdomainid"; public static final String PASSWORD = "password"; public static final String NEW_PASSWORD = "new_password"; @@ -159,6 +162,7 @@ public class ApiConstants { public static final String SCHEDULE = "schedule"; public static final String SCOPE = "scope"; public static final String SECRET_KEY = "usersecretkey"; + public static final String SINCE = "since"; public static final String KEY = "key"; public static final String SEARCH_BASE = "searchbase"; public static final String SECURITY_GROUP_IDS = "securitygroupids"; @@ -324,6 +328,7 @@ public class ApiConstants { public static final String SOURCE_NAT_SUPPORTED = "sourcenatsupported"; public static final String RESOURCE_STATE = "resourcestate"; public static final String PROJECT_INVITE_REQUIRED = "projectinviterequired"; + public static final String REQUIRED = "required"; public static final String RESTART_REQUIRED = "restartrequired"; public static final String ALLOW_USER_CREATE_PROJECTS = "allowusercreateprojects"; public static final String CONSERVE_MODE = "conservemode"; diff --git a/api/src/org/apache/cloudstack/discovery/ApiDiscoveryService.java b/api/src/org/apache/cloudstack/discovery/ApiDiscoveryService.java new file mode 100644 index 00000000000..12206949db3 --- /dev/null +++ b/api/src/org/apache/cloudstack/discovery/ApiDiscoveryService.java @@ -0,0 +1,29 @@ +// 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.discovery; + +import com.cloud.utils.component.Adapter; +import com.cloud.utils.component.PluggableService; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.response.ListResponse; + +import java.util.Map; + +public interface ApiDiscoveryService extends Adapter, PluggableService { + ListResponse listApis(); + Map> getApiNameCmdClassMapping(); +} diff --git a/client/pom.xml b/client/pom.xml index b703cbe966f..1bbae1f7d08 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -30,6 +30,11 @@ cloud-plugin-acl-static-role-based ${project.version} + + org.apache.cloudstack + cloud-plugin-api-discovery + ${project.version} + org.apache.cloudstack cloud-plugin-user-authenticator-ldap diff --git a/client/tomcatconf/api-discovery_commands.properties.in b/client/tomcatconf/api-discovery_commands.properties.in new file mode 100644 index 00000000000..49ddfde42d8 --- /dev/null +++ b/client/tomcatconf/api-discovery_commands.properties.in @@ -0,0 +1,23 @@ +# 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. + +# bitmap of permissions at the end of each classname, 1 = ADMIN, 2 = +# RESOURCE_DOMAIN_ADMIN, 4 = DOMAIN_ADMIN, 8 = USER +# Please standardize naming conventions to camel-case (even for acronyms). + +# CloudStack API Discovery service command +listApis=15 diff --git a/client/tomcatconf/components.xml.in b/client/tomcatconf/components.xml.in index a7378bd9f49..b9feed15a88 100755 --- a/client/tomcatconf/components.xml.in +++ b/client/tomcatconf/components.xml.in @@ -56,6 +56,9 @@ under the License. + + + @@ -177,6 +180,7 @@ under the License. + diff --git a/plugins/api/discovery/pom.xml b/plugins/api/discovery/pom.xml new file mode 100644 index 00000000000..a61b275addc --- /dev/null +++ b/plugins/api/discovery/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + cloud-plugin-api-discovery + Apache CloudStack Plugin - API Discovery + + org.apache.cloudstack + cloudstack-plugins + 4.1.0-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-api + ${project.version} + + + org.apache.cloudstack + cloud-utils + ${project.version} + + + diff --git a/plugins/api/discovery/src/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java b/plugins/api/discovery/src/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java new file mode 100644 index 00000000000..a7e60e340b2 --- /dev/null +++ b/plugins/api/discovery/src/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java @@ -0,0 +1,55 @@ +// 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.api.command.user.discovery; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.PlugService; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.discovery.ApiDiscoveryService; +import org.apache.cloudstack.api.response.ApiDiscoveryResponse; + +import org.apache.log4j.Logger; + +@APICommand(name = "listApis", responseObject = ApiDiscoveryResponse.class, description = "lists all apis available to the user as per their account type", since = "4.1.0") +public class ListApisCmd extends BaseListCmd { + + public static final Logger s_logger = Logger.getLogger(ListApisCmd.class.getName()); + private static final String s_name = "listapisresponse"; + + @PlugService + ApiDiscoveryService _apiDiscoveryService; + + @Override + public void execute() throws ServerApiException { + if (_apiDiscoveryService != null) { + ListResponse response = (ListResponse) _apiDiscoveryService.listApis(); + if (response == null) { + throw new ServerApiException(BaseCmd.INTERNAL_ERROR, "Api Discovery plugin was unable to find and process any apis"); + } + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + } + + @Override + public String getCommandName() { + return s_name; + } +} diff --git a/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiDiscoveryResponse.java b/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiDiscoveryResponse.java new file mode 100644 index 00000000000..b6a4f124954 --- /dev/null +++ b/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiDiscoveryResponse.java @@ -0,0 +1,75 @@ +// 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.api.response; + +import com.cloud.user.Account; +import org.apache.cloudstack.api.ApiConstants; +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +import java.util.HashSet; +import java.util.Set; + +@SuppressWarnings("unused") +@EntityReference(value = Account.class) +public class ApiDiscoveryResponse extends BaseResponse { + @SerializedName(ApiConstants.NAME) @Param(description="the name of the api command") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) @Param(description="description of the api") + private String description; + + @SerializedName(ApiConstants.SINCE) @Param(description="version of CloudStack the api was introduced in") + private String since; + + @SerializedName(ApiConstants.IS_ASYNC) @Param(description="true if api is asynchronous") + private Boolean isAsync; + + @SerializedName(ApiConstants.PARAMS) @Param(description="the list params the api accepts", responseObject = ApiParameterResponse.class) + private Set params; + + public ApiDiscoveryResponse(){ + params = new HashSet(); + isAsync = false; + } + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setSince(String since) { + this.since = since; + } + + public void setAsync(Boolean isAsync) { + this.isAsync = isAsync; + } + + public void setParams(Set params) { + this.params = params; + } + + public void addParam(ApiParameterResponse param) { + this.params.add(param); + } +} \ No newline at end of file diff --git a/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiParameterResponse.java b/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiParameterResponse.java new file mode 100644 index 00000000000..9a73bec3dee --- /dev/null +++ b/plugins/api/discovery/src/org/apache/cloudstack/api/response/ApiParameterResponse.java @@ -0,0 +1,70 @@ +// 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.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.BaseResponse; + +public class ApiParameterResponse extends BaseResponse { + @SerializedName(ApiConstants.NAME) @Param(description="the name of the api parameter") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) @Param(description="description of the api parameter") + private String description; + + @SerializedName(ApiConstants.TYPE) @Param(description="parameter type") + private String type; + + @SerializedName(ApiConstants.LENGTH) @Param(description="length of the parameter") + private int length; + + @SerializedName(ApiConstants.REQUIRED) @Param(description="version of CloudStack the api was introduced in") + private Boolean required; + + @SerializedName(ApiConstants.SINCE) @Param(description="version of CloudStack the api was introduced in") + private String since; + + public ApiParameterResponse(){ + } + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setType(String type) { + this.type = type; + } + + public void setLength(int length) { + this.length = length; + } + + public void setRequired(Boolean required) { + this.required = required; + } + + public void setSince(String since) { + this.since = since; + } + + } \ No newline at end of file diff --git a/plugins/api/discovery/src/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java b/plugins/api/discovery/src/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java new file mode 100644 index 00000000000..ca57df5d314 --- /dev/null +++ b/plugins/api/discovery/src/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java @@ -0,0 +1,144 @@ +// 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.discovery; + +import com.cloud.utils.ReflectUtil; +import com.cloud.utils.component.AdapterBase; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseAsyncCreateCmd; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ApiDiscoveryResponse; +import org.apache.cloudstack.api.response.ApiParameterResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.log4j.Logger; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Local(value = ApiDiscoveryService.class) +public class ApiDiscoveryServiceImpl extends AdapterBase implements ApiDiscoveryService { + + private static final Logger s_logger = Logger.getLogger(ApiDiscoveryServiceImpl.class); + private Map> _apiNameCmdClassMap; + private ListResponse _discoveryResponse; + + protected ApiDiscoveryServiceImpl() { + super(); + } + + private void generateApiNameCmdClassMapping() { + _apiNameCmdClassMap = new HashMap>(); + Set> cmdClasses = ReflectUtil.getClassesWithAnnotation(APICommand.class, new String[]{"org.apache.cloudstack.api", "com.cloud.api"}); + + for(Class cmdClass: cmdClasses) { + String apiName = cmdClass.getAnnotation(APICommand.class).name(); + if (_apiNameCmdClassMap.containsKey(apiName)) { + s_logger.error("API Cmd class " + cmdClass.getName() + " has non-unique apiname" + apiName); + continue; + } + _apiNameCmdClassMap.put(apiName, cmdClass); + } + } + + private void precacheListApiResponse() { + + if(_apiNameCmdClassMap == null) + return; + + _discoveryResponse = new ListResponse(); + + List apiDiscoveryResponses = new ArrayList(); + + for(String key: _apiNameCmdClassMap.keySet()) { + Class cmdClass = _apiNameCmdClassMap.get(key); + APICommand apiCmdAnnotation = cmdClass.getAnnotation(APICommand.class); + if (apiCmdAnnotation == null) + apiCmdAnnotation = cmdClass.getSuperclass().getAnnotation(APICommand.class); + if (apiCmdAnnotation == null + || !apiCmdAnnotation.includeInApiDoc() + || apiCmdAnnotation.name().isEmpty()) + continue; + + ApiDiscoveryResponse response = new ApiDiscoveryResponse(); + response.setName(apiCmdAnnotation.name()); + response.setDescription(apiCmdAnnotation.description()); + response.setSince(apiCmdAnnotation.since()); + + Field[] fields = ReflectUtil.getAllFieldsForClass(cmdClass, + new Class[] {BaseCmd.class, BaseAsyncCmd.class, BaseAsyncCreateCmd.class}); + + boolean isAsync = ReflectUtil.isCmdClassAsync(cmdClass, + new Class[] {BaseAsyncCmd.class, BaseAsyncCreateCmd.class}); + + response.setAsync(isAsync); + + for(Field field: fields) { + Parameter parameterAnnotation = field.getAnnotation(Parameter.class); + if (parameterAnnotation != null + && parameterAnnotation.expose() + && parameterAnnotation.includeInApiDoc()) { + + ApiParameterResponse paramResponse = new ApiParameterResponse(); + paramResponse.setName(parameterAnnotation.name()); + paramResponse.setDescription(parameterAnnotation.description()); + paramResponse.setType(parameterAnnotation.type().toString()); + paramResponse.setLength(parameterAnnotation.length()); + paramResponse.setRequired(parameterAnnotation.required()); + paramResponse.setSince(parameterAnnotation.since()); + response.addParam(paramResponse); + } + } + response.setObjectName("apis"); + apiDiscoveryResponses.add(response); + } + _discoveryResponse.setResponses(apiDiscoveryResponses); + } + + @Override + public boolean configure(String name, Map params) + throws ConfigurationException { + super.configure(name, params); + + generateApiNameCmdClassMapping(); + precacheListApiResponse(); + + return true; + } + + public Map> getApiNameCmdClassMapping() { + return _apiNameCmdClassMap; + } + + @Override + public ListResponse listApis() { + return _discoveryResponse; + } + + @Override + public String[] getPropertiesFiles() { + return new String[]{"api-discovery_commands.properties"}; + } +} \ No newline at end of file diff --git a/plugins/pom.xml b/plugins/pom.xml index f0589a1caaf..a42ae2967b1 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -32,6 +32,7 @@ test + api/discovery acl/static-role-based deployment-planners/user-concentrated-pod deployment-planners/user-dispersing diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java index e2dc9ce2bbf..8d304e0c9b3 100755 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@ -50,6 +50,7 @@ import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import com.cloud.utils.ReflectUtil; import org.apache.cloudstack.acl.APIAccessChecker; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.*; @@ -59,6 +60,7 @@ import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.api.command.user.vm.ListVMsCmd; import org.apache.cloudstack.api.command.user.vmgroup.ListVMGroupsCmd; import org.apache.cloudstack.api.command.user.volume.ListVolumesCmd; +import org.apache.cloudstack.discovery.ApiDiscoveryService; import org.apache.commons.codec.binary.Base64; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.ConnectionClosedException; @@ -145,8 +147,11 @@ public class ApiServer implements HttpRequestHandler { @Inject private AccountManager _accountMgr = null; @Inject private DomainManager _domainMgr = null; @Inject private AsyncJobManager _asyncMgr = null; + @Inject(adapter = APIAccessChecker.class) protected Adapters _apiAccessCheckers; + @Inject(adapter = ApiDiscoveryService.class) + protected Adapters _apiDiscoveryServices; private Account _systemAccount = null; private User _systemUser = null; @@ -201,17 +206,8 @@ public class ApiServer implements HttpRequestHandler { } } - // Populate api name and cmd class mappings - Reflections reflections = new Reflections("org.apache.cloudstack.api"); - Set> cmdClasses = reflections.getTypesAnnotatedWith(APICommand.class); - reflections = new Reflections("com.cloud.api"); - cmdClasses.addAll(reflections.getTypesAnnotatedWith(APICommand.class)); - for(Class cmdClass: cmdClasses) { - String apiName = cmdClass.getAnnotation(APICommand.class).name(); - if (_apiNameCmdClassMap.containsKey(apiName)) { - s_logger.error("API Cmd class " + cmdClass.getName() + " has non-unique apiname" + apiName); - } - _apiNameCmdClassMap.put(apiName, cmdClass); + for (ApiDiscoveryService discoveryService: _apiDiscoveryServices) { + _apiNameCmdClassMap.putAll(discoveryService.getApiNameCmdClassMapping()); } encodeApiResponse = Boolean.valueOf(configDao.getValue(Config.EncodeApiResponse.key()));