This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 47c5bb8ee7a709d1ee0e4f2276da2e837137244e
Author: Suresh Kumar Anaparti <[email protected]>
AuthorDate: Mon Apr 13 15:29:34 2026 +0530

    Support list/query async jobs by resource (#12983)
    
    * Add resource filtering to async job query commands
    
    * Fix logical condition in AsyncJobDaoImpl and ResourceIdSupport
    
    * resource type case-insensitive validation
    
    * fix resource type and id search
    
    ---------
    
    Co-authored-by: mprokopchuk <[email protected]>
    Co-authored-by: mprokopchuk <[email protected]>
---
 .../cloudstack/api/ApiCommandResourceType.java     |   4 +-
 .../api/command/user/job/ListAsyncJobsCmd.java     |  15 +++
 .../command/user/job/QueryAsyncJobResultCmd.java   |  18 ++-
 .../cloudstack/framework/jobs/dao/AsyncJobDao.java |  18 +++
 .../framework/jobs/dao/AsyncJobDaoImpl.java        |  33 ++++++
 .../main/java/com/cloud/api/ApiResponseHelper.java |  36 +++++-
 .../java/com/cloud/api/query/QueryManagerImpl.java |  45 +++++---
 .../com/cloud/api/query/ResourceIdSupport.java     | 123 +++++++++++++++++++++
 .../com/cloud/api/query/QueryManagerImplTest.java  |   2 +-
 9 files changed, 267 insertions(+), 27 deletions(-)

diff --git 
a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java 
b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java
index 4d33ba859a5..e2ebb242cbf 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java
@@ -127,8 +127,8 @@ public enum ApiCommandResourceType {
     }
 
     public static ApiCommandResourceType fromString(String value) {
-        if (StringUtils.isNotEmpty(value) && 
EnumUtils.isValidEnum(ApiCommandResourceType.class, value)) {
-            return valueOf(value);
+        if (StringUtils.isNotBlank(value) && 
EnumUtils.isValidEnumIgnoreCase(ApiCommandResourceType.class, value)) {
+            return EnumUtils.getEnumIgnoreCase(ApiCommandResourceType.class, 
value);
         }
         return null;
     }
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
index b55d1b234f1..2c840183113 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
@@ -19,6 +19,7 @@ package org.apache.cloudstack.api.command.user.job;
 import java.util.Date;
 
 import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiArgValidator;
 import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.api.BaseListAccountResourcesCmd;
 import org.apache.cloudstack.api.Parameter;
@@ -40,6 +41,12 @@ public class ListAsyncJobsCmd extends 
BaseListAccountResourcesCmd {
     @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, type = 
CommandType.UUID, entityType = ManagementServerResponse.class, description = 
"The id of the management server", since="4.19")
     private Long managementServerId;
 
+    @Parameter(name = ApiConstants.RESOURCE_ID, validations = 
{ApiArgValidator.UuidString}, type = CommandType.STRING, description = "the ID 
of the resource associated with the job", since="4.22.1")
+    private String resourceId;
+
+    @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, 
description = "the type of the resource associated with the job", 
since="4.22.1")
+    private String resourceType;
+
     /////////////////////////////////////////////////////
     /////////////////// Accessors ///////////////////////
     /////////////////////////////////////////////////////
@@ -52,6 +59,14 @@ public class ListAsyncJobsCmd extends 
BaseListAccountResourcesCmd {
         return managementServerId;
     }
 
+    public String getResourceId() {
+        return resourceId;
+    }
+
+    public String getResourceType() {
+        return resourceType;
+    }
+
     /////////////////////////////////////////////////////
     /////////////// API Implementation///////////////////
     /////////////////////////////////////////////////////
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java
index 93a44375721..5c3b0084574 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java
@@ -16,8 +16,8 @@
 // under the License.
 package org.apache.cloudstack.api.command.user.job;
 
-
 import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiArgValidator;
 import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.api.BaseCmd;
 import org.apache.cloudstack.api.Parameter;
@@ -34,9 +34,15 @@ public class QueryAsyncJobResultCmd extends BaseCmd {
     //////////////// API parameters /////////////////////
     /////////////////////////////////////////////////////
 
-    @Parameter(name = ApiConstants.JOB_ID, type = CommandType.UUID, entityType 
= AsyncJobResponse.class, required = true, description = "The ID of the 
asynchronous job")
+    @Parameter(name = ApiConstants.JOB_ID, type = CommandType.UUID, entityType 
= AsyncJobResponse.class, description = "The ID of the asynchronous job")
     private Long id;
 
+    @Parameter(name = ApiConstants.RESOURCE_ID, validations = 
{ApiArgValidator.UuidString}, type = CommandType.STRING, description = "the ID 
of the resource associated with the job", since="4.22.1")
+    private String resourceId;
+
+    @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, 
description = "the type of the resource associated with the job", 
since="4.22.1")
+    private String resourceType;
+
     /////////////////////////////////////////////////////
     /////////////////// Accessors ///////////////////////
     /////////////////////////////////////////////////////
@@ -45,6 +51,14 @@ public class QueryAsyncJobResultCmd extends BaseCmd {
         return id;
     }
 
+    public String getResourceId() {
+        return resourceId;
+    }
+
+    public String getResourceType() {
+        return resourceType;
+    }
+
     /////////////////////////////////////////////////////
     /////////////// API Implementation///////////////////
     /////////////////////////////////////////////////////
diff --git 
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
 
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
index 9f7a4ad6e05..926280bfead 100644
--- 
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
+++ 
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
@@ -23,12 +23,30 @@ import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
 
 import com.cloud.utils.db.GenericDao;
 
+import javax.annotation.Nullable;
+
 public interface AsyncJobDao extends GenericDao<AsyncJobVO, Long> {
 
     AsyncJobVO findInstancePendingAsyncJob(String instanceType, long 
instanceId);
 
     List<AsyncJobVO> findInstancePendingAsyncJobs(String instanceType, Long 
accountId);
 
+    /**
+     * Finds async job matching the given parameters.
+     * Non-null parameters are added to search criteria.
+     * Returns the most recent job by creation date.
+     * <p>
+     * When searching by resourceId and resourceType, only one active job
+     * is expected per resource, so returning a single result is sufficient.
+     *
+     * @param id           job ID
+     * @param resourceId   resource ID (instanceId)
+     * @param resourceType resource type (instanceType)
+     * @return matching job or null
+     */
+    @Nullable
+    AsyncJobVO findJob(Long id, Long resourceId, String resourceType);
+
     AsyncJobVO findPseudoJob(long threadId, long msid);
 
     void cleanupPseduoJobs(long msid);
diff --git 
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
 
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
index a2f1f36b863..81cc5d4f2a8 100644
--- 
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
+++ 
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
@@ -22,6 +22,8 @@ import java.util.Date;
 import java.util.List;
 
 import org.apache.cloudstack.api.ApiConstants;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
 
 import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
 import org.apache.cloudstack.jobs.JobInfo;
@@ -45,6 +47,7 @@ public class AsyncJobDaoImpl extends 
GenericDaoBase<AsyncJobVO, Long> implements
     private final SearchBuilder<AsyncJobVO> expiringUnfinishedAsyncJobSearch;
     private final SearchBuilder<AsyncJobVO> expiringCompletedAsyncJobSearch;
     private final SearchBuilder<AsyncJobVO> failureMsidAsyncJobSearch;
+    private final SearchBuilder<AsyncJobVO> byIdResourceIdResourceTypeSearch;
     private final GenericSearchBuilder<AsyncJobVO, Long> asyncJobTypeSearch;
     private final GenericSearchBuilder<AsyncJobVO, Long> 
pendingNonPseudoAsyncJobsSearch;
 
@@ -95,6 +98,12 @@ public class AsyncJobDaoImpl extends 
GenericDaoBase<AsyncJobVO, Long> implements
         failureMsidAsyncJobSearch.and("job_cmd", 
failureMsidAsyncJobSearch.entity().getCmd(), Op.IN);
         failureMsidAsyncJobSearch.done();
 
+        byIdResourceIdResourceTypeSearch = createSearchBuilder();
+        byIdResourceIdResourceTypeSearch.and("id", 
byIdResourceIdResourceTypeSearch.entity().getId(), SearchCriteria.Op.EQ);
+        byIdResourceIdResourceTypeSearch.and("instanceId", 
byIdResourceIdResourceTypeSearch.entity().getInstanceId(), 
SearchCriteria.Op.EQ);
+        byIdResourceIdResourceTypeSearch.and("instanceType", 
byIdResourceIdResourceTypeSearch.entity().getInstanceType(), 
SearchCriteria.Op.EQ);
+        byIdResourceIdResourceTypeSearch.done();
+
         asyncJobTypeSearch = createSearchBuilder(Long.class);
         asyncJobTypeSearch.select(null, SearchCriteria.Func.COUNT, 
asyncJobTypeSearch.entity().getId());
         asyncJobTypeSearch.and("job_info", 
asyncJobTypeSearch.entity().getCmdInfo(),Op.LIKE);
@@ -140,6 +149,30 @@ public class AsyncJobDaoImpl extends 
GenericDaoBase<AsyncJobVO, Long> implements
         return listBy(sc);
     }
 
+    @Override
+    public AsyncJobVO findJob(Long id, Long resourceId, String resourceType) {
+        SearchCriteria<AsyncJobVO> sc = 
byIdResourceIdResourceTypeSearch.create();
+
+        if (id == null && resourceId == null && 
StringUtils.isBlank(resourceType)) {
+            logger.debug("findJob called with all null parameters");
+            return null;
+        }
+
+        if (id != null) {
+            sc.setParameters("id", id);
+        }
+        if (resourceId != null && StringUtils.isNotBlank(resourceType)) {
+            sc.setParameters("instanceType", resourceType);
+            sc.setParameters("instanceId", resourceId);
+        }
+        Filter filter = new Filter(AsyncJobVO.class, "created", false, 0L, 1L);
+        List<AsyncJobVO> result = searchIncludingRemoved(sc, filter, 
Boolean.FALSE, false);
+        if (CollectionUtils.isNotEmpty(result)) {
+            return result.get(0);
+        }
+        return null;
+    }
+
     @Override
     public AsyncJobVO findPseudoJob(long threadId, long msid) {
         SearchCriteria<AsyncJobVO> sc = pseudoJobSearch.create();
diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java 
b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
index 60243c50b08..67f83dacaad 100644
--- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java
+++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
@@ -32,6 +32,7 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.function.Consumer;
@@ -39,6 +40,7 @@ import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 
+import com.cloud.api.query.ResourceIdSupport;
 import com.cloud.bgp.ASNumber;
 import com.cloud.bgp.ASNumberRange;
 import com.cloud.configuration.ConfigurationService;
@@ -57,6 +59,7 @@ import org.apache.cloudstack.affinity.AffinityGroup;
 import org.apache.cloudstack.affinity.AffinityGroupResponse;
 import org.apache.cloudstack.annotation.AnnotationService;
 import org.apache.cloudstack.annotation.dao.AnnotationDao;
+import org.apache.cloudstack.api.ApiCommandResourceType;
 import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.api.ApiConstants.DomainDetails;
 import org.apache.cloudstack.api.ApiConstants.HostDetails;
@@ -219,6 +222,7 @@ import 
org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
 import org.apache.cloudstack.framework.jobs.AsyncJob;
 import org.apache.cloudstack.framework.jobs.AsyncJobManager;
+import org.apache.cloudstack.framework.jobs.dao.AsyncJobDao;
 import org.apache.cloudstack.gui.theme.GuiThemeJoin;
 import org.apache.cloudstack.management.ManagementServerHost;
 import org.apache.cloudstack.network.BgpPeerVO;
@@ -447,7 +451,7 @@ import com.cloud.vm.snapshot.dao.VMSnapshotDao;
 
 import sun.security.x509.X509CertImpl;
 
-public class ApiResponseHelper implements ResponseGenerator {
+public class ApiResponseHelper implements ResponseGenerator, ResourceIdSupport 
{
 
     protected Logger logger = LogManager.getLogger(ApiResponseHelper.class);
     private static final DecimalFormat s_percentFormat = new 
DecimalFormat("##.##");
@@ -529,6 +533,8 @@ public class ApiResponseHelper implements ResponseGenerator 
{
     RoutedIpv4Manager routedIpv4Manager;
     @Inject
     ResourceIconManager resourceIconManager;
+    @Inject
+    AsyncJobDao asyncJobDao;
 
     public static String getPrettyDomainPath(String path) {
         if (path == null) {
@@ -2304,16 +2310,26 @@ public class ApiResponseHelper implements 
ResponseGenerator {
 
     @Override
     public AsyncJobResponse queryJobResult(final QueryAsyncJobResultCmd cmd) {
-        final Account caller = CallContext.current().getCallingAccount();
+        ApiCommandResourceType resourceType = 
getResourceType(cmd.getResourceType());
+        String resourceTypeName = 
Optional.ofNullable(resourceType).map(ApiCommandResourceType::name).orElse(null);
+
+        Long resourceId = getResourceId(resourceType, cmd.getResourceId());
+        Long jobId = cmd.getId();
+        if (jobId == null && resourceId == null) {
+            throw new InvalidParameterValueException("Expected parameter job 
id or parameters resource type and resource id");
+        }
 
-        final AsyncJob job = 
_entityMgr.findByIdIncludingRemoved(AsyncJob.class, cmd.getId());
+        final AsyncJob job = asyncJobDao.findJob(jobId, resourceId, 
resourceTypeName);
         if (job == null) {
-            throw new InvalidParameterValueException("Unable to find a job by 
id " + cmd.getId());
+            throw new InvalidParameterValueException("Unable to find a job by 
id " + jobId + " resource type "
+                    + cmd.getResourceType() + " resource id " + 
cmd.getResourceId());
         }
+        jobId = job.getId();
 
         final User userJobOwner = 
_accountMgr.getUserIncludingRemoved(job.getUserId());
         final Account jobOwner = 
_accountMgr.getAccount(userJobOwner.getAccountId());
 
+        final Account caller = CallContext.current().getCallingAccount();
         //check permissions
         if (_accountMgr.isNormalUser(caller.getId())) {
             //regular users can see only jobs they own
@@ -2324,7 +2340,7 @@ public class ApiResponseHelper implements 
ResponseGenerator {
             _accountMgr.checkAccess(caller, null, true, jobOwner);
         }
 
-        return createAsyncJobResponse(_jobMgr.queryJob(cmd.getId(), true));
+        return createAsyncJobResponse(_jobMgr.queryJob(jobId, true));
     }
 
     public AsyncJobResponse createAsyncJobResponse(AsyncJob job) {
@@ -5704,4 +5720,14 @@ protected Map<String, ResourceIcon> 
getResourceIconsUsingOsCategory(List<Templat
         consoleSessionResponse.setObjectName("consolesession");
         return consoleSessionResponse;
     }
+
+    @Override
+    public EntityManager getEntityManager() {
+        return _entityMgr;
+    }
+
+    @Override
+    public AccountManager getAccountManager() {
+        return _accountMgr;
+    }
 }
diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java 
b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
index fea87b66fed..6214a8f2743 100644
--- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
+++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
@@ -31,7 +31,6 @@ import java.util.ListIterator;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.UUID;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -370,7 +369,7 @@ import com.cloud.vm.dao.VMInstanceDao;
 import com.cloud.vm.dao.VMInstanceDetailsDao;
 
 @Component
-public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements 
QueryService, Configurable {
+public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements 
QueryService, Configurable, ResourceIdSupport {
 
 
     private static final String ID_FIELD = "id";
@@ -869,26 +868,14 @@ public class QueryManagerImpl extends 
MutualExclusiveIdsManagerBase implements Q
         Integer entryTime = cmd.getEntryTime();
         Integer duration = cmd.getDuration();
         Long startId = cmd.getStartId();
-        final String resourceUuid = cmd.getResourceId();
-        final String resourceTypeStr = cmd.getResourceType();
+        final String resourceUuid = getResourceUuid(cmd.getResourceId());
+        final ApiCommandResourceType resourceType = 
getResourceType(cmd.getResourceType());
         final String stateStr = cmd.getState();
-        ApiCommandResourceType resourceType = null;
         Long resourceId = null;
-        if (resourceTypeStr != null) {
-            resourceType = ApiCommandResourceType.fromString(resourceTypeStr);
-            if (resourceType == null) {
-                throw new 
InvalidParameterValueException(String.format("Invalid %s", 
ApiConstants.RESOURCE_TYPE));
-            }
-        }
         if (resourceUuid != null) {
-            if (resourceTypeStr == null) {
+            if (resourceType == null) {
                 throw new InvalidParameterValueException(String.format("%s 
parameter must be used with %s parameter", ApiConstants.RESOURCE_ID, 
ApiConstants.RESOURCE_TYPE));
             }
-            try {
-                UUID.fromString(resourceUuid);
-            } catch (IllegalArgumentException ex) {
-                throw new 
InvalidParameterValueException(String.format("Invalid %s", 
ApiConstants.RESOURCE_ID));
-            }
             Object object = 
entityManager.findByUuidIncludingRemoved(resourceType.getAssociatedClass(), 
resourceUuid);
             if (object instanceof InternalIdentity) {
                 resourceId = ((InternalIdentity)object).getId();
@@ -3205,6 +3192,20 @@ public class QueryManagerImpl extends 
MutualExclusiveIdsManagerBase implements Q
             sc.setParameters("executingMsid", msHost.getMsid());
         }
 
+        if (cmd.getResourceType() != null) {
+            ApiCommandResourceType resourceType = 
getResourceType(cmd.getResourceType());
+            sc.addAnd("instanceType", SearchCriteria.Op.EQ, 
resourceType.toString());
+
+            final String resourceId = getResourceUuid(cmd.getResourceId());
+            if (resourceId == null) {
+                throw new InvalidParameterValueException("Invalid resource id 
for the resource type " + resourceType);
+            }
+
+            sc.addAnd("instanceUuid", SearchCriteria.Op.EQ, resourceId);
+        } else if (cmd.getResourceId() != null) {
+            throw new InvalidParameterValueException("Resource type must be 
specified for the resource id");
+        }
+
         return _jobJoinDao.searchAndCount(sc, searchFilter);
     }
 
@@ -6288,4 +6289,14 @@ public class QueryManagerImpl extends 
MutualExclusiveIdsManagerBase implements Q
         return new ConfigKey<?>[] {AllowUserViewDestroyedVM, 
UserVMDeniedDetails, UserVMReadOnlyDetails, SortKeyAscending,
                 AllowUserViewAllDomainAccounts, AllowUserViewAllDataCenters, 
SharePublicTemplatesWithOtherDomains, ReturnVmStatsOnVmList};
     }
+
+    @Override
+    public EntityManager getEntityManager() {
+        return entityManager;
+    }
+
+    @Override
+    public AccountManager getAccountManager() {
+        return accountMgr;
+    }
 }
diff --git a/server/src/main/java/com/cloud/api/query/ResourceIdSupport.java 
b/server/src/main/java/com/cloud/api/query/ResourceIdSupport.java
new file mode 100644
index 00000000000..2c32df22f0c
--- /dev/null
+++ b/server/src/main/java/com/cloud/api/query/ResourceIdSupport.java
@@ -0,0 +1,123 @@
+// 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.query;
+
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.user.Account;
+import com.cloud.user.AccountManager;
+import com.cloud.utils.db.EntityManager;
+import org.apache.cloudstack.acl.ControlledEntity;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.InternalIdentity;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Optional;
+import java.util.UUID;
+import static org.apache.cloudstack.acl.SecurityChecker.AccessType;
+
+/**
+ * Support interface for converting resource UUIDs to internal IDs
+ * with validation and access control.
+ *
+ * @author mprokopchuk
+ */
+public interface ResourceIdSupport {
+
+    EntityManager getEntityManager();
+
+    AccountManager getAccountManager();
+
+    /**
+     * Converts resource UUID to internal database ID with access control 
checks.
+     *
+     * @param resourceType type of the resource
+     * @param resourceUuid UUID of the resource
+     * @return internal resource ID or null if parameters are null
+     * @throws InvalidParameterValueException if only one parameter provided 
or resource not found
+     */
+    default Long getResourceId(ApiCommandResourceType resourceType, String 
resourceUuid) {
+        String uuid = getResourceUuid(resourceUuid);
+
+        if (resourceType == null && uuid == null) {
+            return null;
+        } else if ((resourceType == null) ^ (uuid == null)) {
+            throw new InvalidParameterValueException(String.format("Both %s 
and %s required",
+                    ApiConstants.RESOURCE_ID, ApiConstants.RESOURCE_TYPE));
+        }
+
+        Object object = 
getEntityManager().findByUuidIncludingRemoved(resourceType.getAssociatedClass(),
 resourceUuid);
+        if (!(object instanceof InternalIdentity)) {
+            throw new InvalidParameterValueException(String.format("Invalid 
%s", ApiConstants.RESOURCE_ID));
+        }
+        Long resourceId = ((InternalIdentity) object).getId();
+
+        Account caller = CallContext.current().getCallingAccount();
+        boolean isRootAdmin = getAccountManager().isRootAdmin(caller.getId());
+
+        if (!isRootAdmin && object instanceof ControlledEntity) {
+            ControlledEntity entity = (ControlledEntity) object;
+            boolean sameOwner = entity.getAccountId() == caller.getId();
+            getAccountManager().checkAccess(caller, AccessType.ListEntry, 
sameOwner, entity);
+        }
+
+        return resourceId;
+    }
+
+    /**
+     * Parses and validates resource type string.
+     *
+     * @param resourceType resource type as string
+     * @return parsed resource type or null if not provided
+     * @throws InvalidParameterValueException if provided type is invalid
+     */
+    default ApiCommandResourceType getResourceType(String resourceType) {
+        Optional<String> resourceTypeOpt = 
Optional.ofNullable(resourceType).filter(StringUtils::isNotBlank);
+        // return null if resource type was not provided
+        if (resourceTypeOpt.isEmpty()) {
+            return null;
+        }
+        // return value or throw exception if provided resource type is invalid
+        return resourceTypeOpt
+                .map(ApiCommandResourceType::fromString)
+                .orElseThrow(() -> new 
InvalidParameterValueException(String.format("Invalid %s",
+                        ApiConstants.RESOURCE_TYPE)));
+    }
+
+    /**
+     * Validates resource UUID format.
+     *
+     * @param resourceUuid UUID string to validate
+     * @return validated UUID or null if not provided
+     * @throws InvalidParameterValueException if UUID format is invalid
+     */
+    default String getResourceUuid(String resourceUuid) {
+        if (StringUtils.isBlank(resourceUuid)) {
+            return null;
+        }
+
+        try {
+            UUID.fromString(resourceUuid);
+        } catch (IllegalArgumentException ex) {
+            throw new InvalidParameterValueException(String.format("Invalid 
%s", ApiConstants.RESOURCE_ID));
+        }
+
+        return resourceUuid;
+    }
+
+}
diff --git a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java 
b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java
index 750f4d8655b..ccc0d0c8d4c 100644
--- a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java
+++ b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java
@@ -272,7 +272,7 @@ public class QueryManagerImplTest {
     public void searchForEventsFailResourceIdInvalid() {
         ListEventsCmd cmd  = setupMockListEventsCmd();
         Mockito.when(cmd.getResourceId()).thenReturn("random");
-        
Mockito.when(cmd.getResourceType()).thenReturn(ApiCommandResourceType.VirtualMachine.toString());
+        
Mockito.lenient().when(cmd.getResourceType()).thenReturn(ApiCommandResourceType.VirtualMachine.toString());
         queryManager.searchForEvents(cmd);
     }
 

Reply via email to