/* * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. 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.wso2.carbon.bpmn.people.substitution; import org.activiti.engine.*; import org.activiti.engine.task.IdentityLink; import org.activiti.engine.task.IdentityLinkType; import org.activiti.engine.task.Task; import org.activiti.engine.task.TaskQuery; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.wso2.carbon.bpmn.core.BPMNConstants; import org.wso2.carbon.bpmn.core.BPMNServerHolder; import org.wso2.carbon.bpmn.core.mgt.dao.ActivitiDAO; import org.wso2.carbon.bpmn.core.mgt.model.PaginatedSubstitutesDataModel; import org.wso2.carbon.bpmn.core.mgt.model.SubstitutesDataModel; import org.wso2.carbon.bpmn.core.utils.BPMNActivitiConfiguration; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.utils.multitenancy.MultitenantUtils; import java.util.*; import java.util.concurrent.*; public class UserSubstitutionUtils { private static final Log log = LogFactory.getLog(UserSubstitutionUtils.class); public static final String LIST_SEPARATOR = ","; public static final String TRUE = "true"; /** * Persist the substitute info. Transitive substitute is not added here. * @param assignee - User becomes unavailable * @param substitute - substitute for the assignee * @param startDate - start od the substitution * @param endDate - end of the substitution * @param taskListString - Comma separated String of task Ids * @return added row count. * @throws SubstitutionException */ public static SubstitutesDataModel addSubstituteInfo(String assignee, String substitute, Date startDate, Date endDate, String taskListString, int tenantId) throws SubstitutionException { ActivitiDAO activitiDAO = SubstitutionDataHolder.getInstance().getActivitiDAO(); //at any given time there could be only one substitute for a single user if (activitiDAO.selectSubstituteInfo(assignee, tenantId) != null) { log.error("Substitute for user: " + assignee + ", already exist. Try to update the substitute info"); throw new SubstitutionException( "Substitute for user: " + assignee + ", already exist. Try to update the substitute info"); } else { SubstitutesDataModel dataModel = new SubstitutesDataModel(); dataModel.setUser(assignee); dataModel.setSubstitute(MultitenantUtils.getTenantAwareUsername(substitute)); dataModel.setSubstitutionStart(startDate); if (endDate == null) { endDate = getEndTimeMaxDate(); } dataModel.setSubstitutionEnd(endDate); dataModel.setEnabled(true); //by default enabled dataModel.setCreated(new Date()); dataModel.setTenantId(tenantId); dataModel.setTaskList(taskListString); activitiDAO.insertSubstitute(dataModel); return dataModel; } } /** * Handles addition of new substitute record and it's post conditions. * @param assignee * @param substitute * @param startTime * @param endTime * @param enabled * @param taskList * @throws SubstitutionException */ public static void handleNewSubstituteAddition(String assignee, String substitute, Date startTime, Date endTime, boolean enabled, List<String> taskList, int tenantId) throws SubstitutionException { ActivitiDAO activitiDAO = SubstitutionDataHolder.getInstance().getActivitiDAO(); String taskListStr = getTaskListString(taskList); SubstitutesDataModel dataModel = addSubstituteInfo(assignee, substitute, startTime, endTime, taskListStr, tenantId); if (dataModel.isEnabled() && isBeforeActivationInterval(dataModel.getSubstitutionStart())) { boolean transitivityResolved = updateTransitiveSubstitutes(dataModel, tenantId); if (!transitivityResolved) { //remove added transitive record activitiDAO.removeSubstitute(assignee, tenantId); throw new SubstitutionException( //SubstitutionException "Could not find an available substitute. Use a different user to substitute"); } if (SubstitutionDataHolder.getInstance().isTransitivityEnabled()) { //transitive substitute maybe changed, need to retrieve again. dataModel = activitiDAO.selectSubstituteInfo(dataModel.getUser(), dataModel.getTenantId()); } if (!SubstitutionDataHolder.getInstance().isTransitivityEnabled() || BPMNConstants.TRANSITIVE_SUB_NOT_APPLICABLE .equals(dataModel.getTransitiveSub())) { bulkReassign(dataModel.getUser(), dataModel.getSubstitute(), taskList); } else { bulkReassign(dataModel.getUser(), dataModel.getTransitiveSub(), taskList); } } } /** * Return a LIST_SEPARATOR separated String from given list * @param taskList * @return a LIST_SEPARATOR separated String from given item list */ private static String getTaskListString(List<String> taskList) { if (taskList != null && !taskList.isEmpty()) { StringBuffer list = new StringBuffer(); for (String id : taskList) { list.append(id).append(LIST_SEPARATOR); } return list.toString(); } else { return null; } } /** * Update all the transitive substitute fields if required * @param dataModel - dataModel of user getting unavailable */ private static boolean updateTransitiveSubstitutes(SubstitutesDataModel dataModel, int tenantId) { TransitivityResolver resolver = SubstitutionDataHolder.getInstance().getTransitivityResolver(); if (resolver.isResolvingRequired(dataModel.getUser(), tenantId)) { return resolver.resolveTransitiveSubs(false, tenantId); } else {//need to update transitive sub for this user return resolver.resolveSubstituteForSingleUser(dataModel, tenantId); } } private static boolean isBeforeActivationInterval(Date substitutionStart) { long timeToNextScheduledEvent = BPMNServerHolder.getInstance().getSubstitutionScheduler().getNextScheduledTime(); Date bufferedTime = new Date(System.currentTimeMillis() + timeToNextScheduledEvent); if (substitutionStart.compareTo(bufferedTime) < 0) { return true; } else { return false; } } /** * Reassign the given tasks or all the tasks if the given task list is null, to the given substitute * @param assignee - original user of the tasks * @param substitute - user who getting assigned * @param taskList - list of tasks to reassign. Leave this null to reassign all tha tasks of the assignee. */ public static void bulkReassign(String assignee, String substitute, List<String> taskList) { if (taskList != null) { //reassign the given tasks reassignFromTaskIdsList(taskList, substitute); } else { //reassign all existing tasks for assignee TaskQuery taskQuery = BPMNServerHolder.getInstance().getEngine().getTaskService().createTaskQuery(); taskQuery.taskAssignee(assignee); reassignFromTasksList(taskQuery.list(), substitute); transformUnclaimedTasks(assignee, substitute); } //should mark bulk reassign done } /** * Look for all the tasks the assignee is a candidate and add substitute as a candidate user. * @param assignee * @param substitute */ private static void transformUnclaimedTasks(String assignee, String substitute) { TaskQuery taskQuery = BPMNServerHolder.getInstance().getEngine().getTaskService().createTaskQuery(); taskQuery.taskCandidateUser(assignee); List<Task> candidateTasks = taskQuery.list(); addAsCandidate(candidateTasks, substitute); } /** * Check whether each given task is a candidate for substitution by assignee * @param taskList * @param assignee */ public static boolean validateTasksList(List<String> taskList, String assignee) { if (taskList != null) { TaskQuery taskQuery = BPMNServerHolder.getInstance().getEngine().getTaskService().createTaskQuery(); for (String taskId : taskList) { taskQuery.taskId(taskId); taskQuery.taskAssignee(assignee); List<Task> tasks = taskQuery.list();//this should return a task if valid if (tasks == null || tasks.isEmpty()) { return false; } } } return true; } private static void reassignFromTaskIdsList(final List<String> taskList, final String substitute) { Thread reassignThread = new Thread() { public void run() { for (String taskId : taskList) { BPMNServerHolder.getInstance().getEngine().getTaskService().setAssignee(taskId, substitute); } } }; executeInThreadPool(reassignThread); } private static void executeInThreadPool(Runnable runnable) { ExecutorService pool = Executors.newCachedThreadPool(); pool.execute(runnable); } private static void reassignFromTasksList(final List<Task> taskList, final String substitute) { Thread reassignThread = new Thread() { public void run() { for (Task task : taskList) { BPMNServerHolder.getInstance().getEngine().getTaskService().setAssignee(task.getId(), substitute); } } }; executeInThreadPool(reassignThread); } private static void addAsCandidate(final List<Task> taskList, final String substitute) { if (taskList == null || taskList.isEmpty()) { return; } Thread reassignThread = new Thread() { public void run() { for (Task task : taskList) { BPMNServerHolder.getInstance().getEngine().getTaskService().addCandidateUser(task.getId(), substitute); } } }; executeInThreadPool(reassignThread); } public static void handleUpdateSubstitute(String assignee, String substitute, Date startTime, Date endTime, boolean enabled, List<String> taskList, int tenantId) { ActivitiDAO activitiDAO = SubstitutionDataHolder.getInstance().getActivitiDAO(); SubstitutesDataModel existingSubInfo = activitiDAO.selectSubstituteInfo(assignee, tenantId); if (existingSubInfo != null) { //need to put existing values for null columns, if not existing data may replace by Null if (startTime == null) { startTime = existingSubInfo.getSubstitutionStart(); } if (endTime == null) { endTime = existingSubInfo.getSubstitutionEnd(); } String taskListString = getTaskListString(taskList); if (taskList == null) { taskListString = existingSubInfo.getTaskList(); } SubstitutesDataModel dataModel = updateSubstituteInfo(assignee, substitute, startTime, endTime, taskListString, tenantId); if (dataModel.isEnabled() && isBeforeActivationInterval(dataModel.getSubstitutionStart())) { boolean transitivityResolved = updateTransitiveSubstitutes(dataModel, tenantId); if (!transitivityResolved) { //remove added transitive record activitiDAO.updateSubstituteInfo(existingSubInfo); throw new SubstitutionException( "Could not find an available substitute. Use a different user to substitute"); } if (SubstitutionDataHolder.getInstance().isTransitivityEnabled()) { //transitive substitute maybe changed, need to retrieve again. dataModel = activitiDAO.selectSubstituteInfo(dataModel.getUser(), dataModel.getTenantId()); } if (!SubstitutionDataHolder.getInstance().isTransitivityEnabled() || BPMNConstants.TRANSITIVE_SUB_NOT_APPLICABLE .equals(dataModel.getTransitiveSub())) { bulkReassign(dataModel.getUser(), dataModel.getSubstitute(), taskList); } else { bulkReassign(dataModel.getUser(), dataModel.getTransitiveSub(), taskList); } } } else { throw new SubstitutionException( "Substitute for user: " + assignee + ", does not exist. Try to add new substitute info record"); } } private static SubstitutesDataModel updateSubstituteInfo(String assignee, String substitute, Date startTime, Date endTime, String taskListString, int tenantId) { SubstitutesDataModel dataModel = new SubstitutesDataModel(); dataModel.setUser(assignee); dataModel.setSubstitute(substitute); dataModel.setSubstitutionStart(startTime); dataModel.setSubstitutionEnd(endTime); dataModel.setEnabled(true); //by default enabled dataModel.setTenantId(tenantId); dataModel.setUpdated(new Date()); dataModel.setTaskList(taskListString); SubstitutionDataHolder.getInstance().getActivitiDAO().updateSubstituteInfo(dataModel); return dataModel; } public static void handleChangeSubstitute(String assignee, String substitute, int tenantId) { ActivitiDAO activitiDAO = SubstitutionDataHolder.getInstance().getActivitiDAO(); SubstitutesDataModel existingSubInfo = activitiDAO.selectSubstituteInfo(assignee, tenantId); if (existingSubInfo != null) { activitiDAO.updateSubstitute(assignee, substitute, tenantId, new Date()); if (existingSubInfo.isEnabled() && isBeforeActivationInterval(existingSubInfo.getSubstitutionStart())) { String existingSub = existingSubInfo.getSubstitute(); existingSubInfo.setSubstitute(substitute); boolean transitivityResolved = updateTransitiveSubstitutes(existingSubInfo, tenantId); if (!transitivityResolved) { //remove added record activitiDAO.updateSubstitute(assignee, existingSub, tenantId, existingSubInfo.getUpdated()); throw new SubstitutionException( "Given Substitute is not available. Provide a different user to substitute."); } } } else { throw new SubstitutionException("No substitution record found for the user: " + assignee); } } /** * Get the substitute info of the given user. * @param assignee * @return SubstitutesDataModel */ public static SubstitutesDataModel getSubstituteOfUser(String assignee, int tenantId) { SubstitutesDataModel dataModel = SubstitutionDataHolder.getInstance().getActivitiDAO().selectSubstituteInfo(assignee, tenantId); Date maxDate = getEndTimeMaxDate(); //set null if max end date if (dataModel != null && maxDate.compareTo(dataModel.getSubstitutionEnd()) == 0) { dataModel.setSubstitutionEnd(null); } return dataModel; } private static Date getEndTimeMaxDate() { DateTime dateTime = new DateTime(SubstitutionDataHolder.getInstance().getSubstitutionMaxEpoch(), DateTimeZone.UTC); Date maxDate = new Date(dateTime.toDateTime(DateTimeZone.getDefault()).getMillis()); return maxDate; } /** * Query substitution records by given properties. * Allowed properties: user, substitute, enabled. * Pagination parameters : start, size, sort, order * @param propertiesMap * @return Paginated list of PaginatedSubstitutesDataModel */ public static List<SubstitutesDataModel> querySubstitutions(Map<String, String> propertiesMap, int tenantId) { ActivitiDAO activitiDAO = SubstitutionDataHolder.getInstance().getActivitiDAO(); PaginatedSubstitutesDataModel model = getPaginatedModelFromRequest(propertiesMap, tenantId); String enabled = propertiesMap.get(SubstitutionQueryProperties.ENABLED); boolean enabledProvided = false; if (enabled != null) { enabledProvided = true; } if (!enabledProvided) { return prepareEndTime(activitiDAO.querySubstituteInfoWithoutEnabled(model)); } else { return prepareEndTime(activitiDAO.querySubstituteInfo(model)); } } /** * If the endDate is set to BPMNConstants.SUBSTITUTION_MAX_END_DATE_EPOCH, it is changed to null * @param modelList * @return */ private static List<SubstitutesDataModel> prepareEndTime(List<SubstitutesDataModel> modelList) { for (SubstitutesDataModel model : modelList) { if (model.getSubstitutionEnd()!= null && getEndTimeMaxDate().compareTo(model.getSubstitutionEnd()) == 0) { model.setSubstitutionEnd(null); } } return modelList; } /** * Total count of query substitution result by given properties. * Allowed properties: user, substitute, enabled. * Pagination parameters : start, size, sort, order * @param propertiesMap * @return Total count of query result */ public static int getQueryResultCount(Map<String, String> propertiesMap, int tenantId) { ActivitiDAO activitiDAO = SubstitutionDataHolder.getInstance().getActivitiDAO(); PaginatedSubstitutesDataModel model = getPaginatedModelFromRequest(propertiesMap, tenantId); String enabled = propertiesMap.get(SubstitutionQueryProperties.ENABLED); boolean enabledProvided = false; if (enabled != null) { enabledProvided = true; } if (!enabledProvided) { return activitiDAO.selectQueryResultCountWithoutEnabled(model); } else { return activitiDAO.selectQueryResultCount(model); } } /** * Prepare the paginated data model for a substitution query * @param propertiesMap * @param tenantId * @return PaginatedSubstitutesDataModel */ private static PaginatedSubstitutesDataModel getPaginatedModelFromRequest(Map<String, String> propertiesMap, int tenantId) { PaginatedSubstitutesDataModel model = new PaginatedSubstitutesDataModel(); if (propertiesMap.get(SubstitutionQueryProperties.SUBSTITUTE) != null) { model.setSubstitute(propertiesMap.get(SubstitutionQueryProperties.SUBSTITUTE)); } if (propertiesMap.get(SubstitutionQueryProperties.USER) != null) { model.setUser(propertiesMap.get(SubstitutionQueryProperties.USER)); } String enabled = propertiesMap.get(SubstitutionQueryProperties.ENABLED); if (enabled != null) { if (enabled.equalsIgnoreCase("true")) { model.setEnabled(true); } else if (enabled.equalsIgnoreCase("false")) { model.setEnabled(false); } else { throw new ActivitiIllegalArgumentException("Invalid parameter " + enabled + " for enabled property."); } } model.setTenantId(tenantId); int start = Integer.parseInt(propertiesMap.get(SubstitutionQueryProperties.START)); int size = Integer.parseInt(propertiesMap.get(SubstitutionQueryProperties.SIZE)); model.setStart(start); model.setSize(size); model.setOrder(propertiesMap.get(SubstitutionQueryProperties.ORDER)); model.setSort(propertiesMap.get(SubstitutionQueryProperties.SORT)); return model; } /** * Return the maximum activation interval for a substitution. * @return activation interval in milliseconds */ public static long getActivationInterval() { long activationInterval = BPMNConstants.DEFAULT_SUBSTITUTION_INTERVAL_IN_MINUTES * 60 * 1000; BPMNActivitiConfiguration bpmnActivitiConfiguration = BPMNActivitiConfiguration.getInstance(); if (bpmnActivitiConfiguration != null) { String activationIntervalString = bpmnActivitiConfiguration .getBPMNPropertyValue(BPMNConstants.SUBSTITUTION_CONFIG, BPMNConstants.SUBSTITUTION_SCHEDULER_INTERVAL); if (activationIntervalString != null) { activationInterval = Long.parseLong(activationIntervalString) * 60 * 1000; if (log.isDebugEnabled()) { log.debug("Using the substitution activation interval : " + activationIntervalString + " minutes"); } } } return activationInterval; } public synchronized static boolean handleScheduledEventByTenant(int tenantId) { boolean result = true; TransitivityResolver resolver = SubstitutionDataHolder.getInstance().getTransitivityResolver(); ActivitiDAO activitiDAO = SubstitutionDataHolder.getInstance().getActivitiDAO(); if (SubstitutionDataHolder.getInstance().isTransitivityEnabled()) { result = resolver.resolveTransitiveSubs(true, tenantId); //update transitives, only the map is updated here } else { resolver.subsMap = activitiDAO.selectActiveSubstitutesByTenant(tenantId, new Date(System.currentTimeMillis())); } //bulk reassign //flush into db for (Map.Entry<String, SubstitutesDataModel> entry : resolver.subsMap.entrySet()) { //go through the updated map SubstitutesDataModel model = entry.getValue(); try { //set carbon context PrivilegedCarbonContext.startTenantFlow(); PrivilegedCarbonContext context = PrivilegedCarbonContext.getThreadLocalCarbonContext(); context.setUsername(model.getUser()); context.setTenantId(tenantId, true); if (SubstitutionDataHolder.getInstance().isTransitivityEnabled()) { activitiDAO.updateSubstituteInfo(model); } if (!BPMNConstants.BULK_REASSIGN_PROCESSED.equals(model.getTaskList())) { //active substitution, not yet bulk reassigned String sub = getActualSubstitute(model); if (model.getTaskList() == null) {//reassign all if (sub != null) { bulkReassign(model.getUser(), sub, null); } else {//transitivity undefined, assign to task owner or un-claim assignToTaskOwner(model.getUser(), null); } } else { List<String> taskList = getTaskListFromString(model.getTaskList()); if (sub != null) { bulkReassign(model.getUser(), sub, taskList); } else {//transitivity undefined, assign to task owner or un-claim assignToTaskOwner(model.getUser(), taskList); } } model.setTaskList(BPMNConstants.BULK_REASSIGN_PROCESSED); activitiDAO.updateSubstituteInfo(model); } } finally { PrivilegedCarbonContext.endTenantFlow(); PrivilegedCarbonContext.destroyCurrentContext(); } } //disable expired records disableExpiredRecords(tenantId); return result; } /** * Disable the records that are still enabled but expired * @param tenantId */ public static void disableExpiredRecords(int tenantId) { ActivitiDAO activitiDAO = SubstitutionDataHolder.getInstance().getActivitiDAO(); Map<String, SubstitutesDataModel> map = activitiDAO.getEnabledExpiredRecords(tenantId, new Date(System.currentTimeMillis())); for (Map.Entry<String, SubstitutesDataModel> entry : map.entrySet()) { activitiDAO.enableSubstitution(false, entry.getKey(), tenantId); } } /** * Handle the transitivity resolving, disabling expired records and task reassignments for all substitutions. * @return true if successfully completed */ public static boolean handleScheduledEvent() { //should do this for each tenant that has substitutions List<Integer> tenantList = getTenantsList(); if (tenantList != null && !tenantList.isEmpty()) { for (int tenantId : tenantList) { if (!handleScheduledEventByTenant(tenantId)) { return false; } } } return true; } /** * Get the list of tenants that has substitutions * @return List<Integer> tenantID list */ public static List<Integer> getTenantsList() { return SubstitutionDataHolder.getInstance().getActivitiDAO().getTenantsList(); } private static void assignToTaskOwner(String assignee, List<String> taskList) { TaskService taskService = BPMNServerHolder.getInstance().getEngine().getTaskService(); if (taskList != null) { for (String taskId : taskList) { String taskOwner = null; List<IdentityLink> identityLinks = taskService.getIdentityLinksForTask(taskId); for (IdentityLink link : identityLinks) { if(IdentityLinkType.OWNER.equals(link.getType())) { taskOwner = link.getUserId(); } } if (taskOwner != null) {//assign to task owner taskService.setAssignee(taskId, taskOwner); } else { taskService.addCandidateUser(taskId, assignee); taskService.unclaim(taskId); } } } else {//reassign all tasks TaskQuery taskQuery = taskService.createTaskQuery(); taskQuery.taskAssignee(assignee); List<Task> list = taskQuery.list(); for (Task task : list) { String taskOwner = task.getOwner(); if (taskOwner != null) {//assign to task owner taskService.setAssignee(task.getId(), taskOwner); } else { taskService.addCandidateUser(task.getId(), assignee); taskService.unclaim(task.getId()); } } } } private static List<String> getTaskListFromString(String taskList) { return Arrays.asList(taskList.split("\\s*,\\s*")); } private static String getActualSubstitute(SubstitutesDataModel model) { if (!SubstitutionDataHolder.getInstance().isTransitivityEnabled() || BPMNConstants.TRANSITIVE_SUB_NOT_APPLICABLE .equals(model.getTransitiveSub())) { return model.getSubstitute(); } else if (BPMNConstants.TRANSITIVE_SUB_UNDEFINED.equals(model.getTransitiveSub())){ return null; } else { return model.getTransitiveSub(); } } /** * Check if an active substitution available for given substitute info * @param substitutesDataModel * @return true if substitution active */ private static boolean isSubstitutionActive(SubstitutesDataModel substitutesDataModel) { long startDate = substitutesDataModel.getSubstitutionStart().getTime(); long endDate = substitutesDataModel.getSubstitutionEnd().getTime(); long currentTime = System.currentTimeMillis(); if ((startDate < currentTime) && (endDate > currentTime) && substitutesDataModel.isEnabled() ) { return true; } return false; } /** * Disable the the substitution record of the given assignee * @param disable - true to disable * @param assignee - user of the substitution * @param tenantId - assignee's tenant id */ public static void disableSubstitution(boolean disable, String assignee, int tenantId) { ActivitiDAO activitiDAO = SubstitutionDataHolder.getInstance().getActivitiDAO(); if (activitiDAO.selectSubstituteInfo(assignee, tenantId) != null) { activitiDAO.enableSubstitution(!disable, assignee, tenantId); } else { throw new ActivitiIllegalArgumentException("No substitution record exist for the given user : " + assignee); } } }