diff --git a/common/common-flow/src/main/java/apelet/common/flow/controller/FlowOperationController.java b/common/common-flow/src/main/java/apelet/common/flow/controller/FlowOperationController.java index 82a2258..43d5582 100644 --- a/common/common-flow/src/main/java/apelet/common/flow/controller/FlowOperationController.java +++ b/common/common-flow/src/main/java/apelet/common/flow/controller/FlowOperationController.java @@ -219,7 +219,9 @@ public class FlowOperationController { } FlowWorkOrder workOrder = flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(processInstanceId); FlowEntry flowEntry = workOrder == null ? new FlowEntry() : flowEntryService.getFlowEntryFromCache(workOrder.getProcessDefinitionKey()); - taskInfoVo.setTitle(flowApiService.resolveTitle(flowTaskExt.getTitleTemplate(), processInstanceId, workOrder, flowEntry)); + // 单条详情:直接单查一次扩展对象即可,无 N+1 风险。 + FlowWorkOrderExt workOrderExt = workOrder == null ? null : flowWorkOrderService.getFlowWorkOrderExtByWorkOrderId(workOrder.getWorkOrderId()); + taskInfoVo.setTitle(flowApiService.resolveTitleCached(flowTaskExt.getTitleTemplate(), processInstanceId, workOrder, flowEntry, taskInfoVo.getTaskKey(), workOrderExt)); } return ResponseResult.success(taskInfoVo); } @@ -840,6 +842,11 @@ public class FlowOperationController { flowWorkOrderService.getInList(INSTANCE_ID, instanceIdSet); Map workOrderMap = workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c)); + // 标题快照按工单批量预载(一次 IN 查询覆盖整页),避免逐行查扩展对象造成 N+1。 + Map workOrderExtMap = flowWorkOrderService + .getFlowWorkOrderExtByWorkOrderIds(workOrderList.stream() + .map(FlowWorkOrder::getWorkOrderId).filter(Objects::nonNull).collect(Collectors.toSet())) + .stream().collect(Collectors.toMap(FlowWorkOrderExt::getWorkOrderId, c -> c, (k1, k2) -> k1)); Set processDefinitionIdSet = taskInstanceList.stream() .map(HistoricTaskInstance::getProcessDefinitionId).collect(Collectors.toSet()); List flowEntryPublishList = @@ -872,8 +879,11 @@ public class FlowOperationController { if (flowEntryPublish != null && flowTaskExt != null) { FlowEntry flowEntry = flowEntryMap.get(flowEntryPublish.getEntryId()); if (flowEntry != null) { - result.put("title", flowApiService.resolveTitle( - flowTaskExt.getTitleTemplate(), instanceId, flowWorkOrder, flowEntry)); + Object taskDefinitionKey = result.get("taskDefinitionKey"); + result.put("title", flowApiService.resolveTitleCached( + flowTaskExt.getTitleTemplate(), instanceId, flowWorkOrder, flowEntry, + taskDefinitionKey == null ? null : taskDefinitionKey.toString(), + workOrderExtMap.get(flowWorkOrder.getWorkOrderId()))); } } } @@ -928,6 +938,11 @@ public class FlowOperationController { flowWorkOrderService.getInList(INSTANCE_ID, instanceIdSet); Map workOrderMap = workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c)); + // 标题快照按工单批量预载(一次 IN 查询覆盖整页),避免逐行查扩展对象造成 N+1。 + Map workOrderExtMap = flowWorkOrderService + .getFlowWorkOrderExtByWorkOrderIds(workOrderList.stream() + .map(FlowWorkOrder::getWorkOrderId).filter(Objects::nonNull).collect(Collectors.toSet())) + .stream().collect(Collectors.toMap(FlowWorkOrderExt::getWorkOrderId, c -> c, (k1, k2) -> k1)); Set processDefinitionIdSet = pageData.getDataList().stream() .map(HistoricProcessInstance::getProcessDefinitionId).collect(Collectors.toSet()); Map flowEntryPublishMap = new HashMap<>(); @@ -974,8 +989,10 @@ public class FlowOperationController { if (flowEntryPublish != null && flowTaskExt != null) { FlowEntry flowEntry = flowEntryMap.get(flowEntryPublish.getEntryId()); if (flowEntry != null) { - data.put("title", flowApiService.resolveTitle( - flowTaskExt.getTitleTemplate(), instance.getId(), workOrder, flowEntry)); + data.put("title", flowApiService.resolveTitleCached( + flowTaskExt.getTitleTemplate(), instance.getId(), workOrder, flowEntry, + initTaskKeyMap.get(instance.getProcessDefinitionId()), + workOrderExtMap.get(workOrder.getWorkOrderId()))); } } } diff --git a/common/common-flow/src/main/java/apelet/common/flow/listener/FlowUserTaskListener.java b/common/common-flow/src/main/java/apelet/common/flow/listener/FlowUserTaskListener.java index f818bf0..e35e5bd 100644 --- a/common/common-flow/src/main/java/apelet/common/flow/listener/FlowUserTaskListener.java +++ b/common/common-flow/src/main/java/apelet/common/flow/listener/FlowUserTaskListener.java @@ -2,6 +2,9 @@ package apelet.common.flow.listener; import apelet.common.core.util.ApplicationContextHolder; import apelet.common.flow.constant.FlowConstant; +import apelet.common.flow.model.FlowWorkOrder; +import apelet.common.flow.service.FlowApiService; +import apelet.common.flow.service.FlowWorkOrderService; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.RuntimeService; import org.flowable.engine.delegate.TaskListener; @@ -21,6 +24,12 @@ public class FlowUserTaskListener implements TaskListener { private final transient RuntimeService runtimeService = ApplicationContextHolder.getBean(RuntimeService.class); + private final transient FlowWorkOrderService flowWorkOrderService = + ApplicationContextHolder.getBean(FlowWorkOrderService.class); + + private final transient FlowApiService flowApiService = + ApplicationContextHolder.getBean(FlowApiService.class); + @Override public void notify(DelegateTask delegateTask) { Map variables = delegateTask.getVariables(); @@ -28,5 +37,28 @@ public class FlowUserTaskListener implements TaskListener { delegateTask.setAssignee(variables.get(FlowConstant.DELEGATE_ASSIGNEE_VAR).toString()); runtimeService.removeVariableLocal(delegateTask.getExecutionId(), FlowConstant.DELEGATE_ASSIGNEE_VAR); } + this.buildTaskTitle(delegateTask); + } + + /** + * 任务创建时按该节点配置的标题模板生成标题,写入工单扩展的 title_json,查询时直接读取。 + *

首节点的 create 事件发生在工单创建之前(工单在 flowApiService.start 返回之后才建),此处取不到工单, + * 交由 FlowWorkOrderService#saveNew 在建单后补写,所以这里直接返回。

+ */ + private void buildTaskTitle(DelegateTask delegateTask) { + try { + FlowWorkOrder workOrder = + flowWorkOrderService.getFlowWorkOrderByProcessInstanceId(delegateTask.getProcessInstanceId()); + if (workOrder == null) { + // 首节点:工单尚未创建,交由 saveNew 补写。 + return; + } + flowApiService.buildAndSaveTaskTitle( + workOrder, delegateTask.getProcessDefinitionId(), delegateTask.getTaskDefinitionKey()); + } catch (Exception e) { + // 标题属于展示字段,生成失败不能阻断流程流转。 + log.warn("生成节点标题快照失败。processInstanceId [{}], taskKey [{}]", + delegateTask.getProcessInstanceId(), delegateTask.getTaskDefinitionKey(), e); + } } } diff --git a/common/common-flow/src/main/java/apelet/common/flow/model/FlowWorkOrderExt.java b/common/common-flow/src/main/java/apelet/common/flow/model/FlowWorkOrderExt.java index 4d571d1..8731ccd 100644 --- a/common/common-flow/src/main/java/apelet/common/flow/model/FlowWorkOrderExt.java +++ b/common/common-flow/src/main/java/apelet/common/flow/model/FlowWorkOrderExt.java @@ -40,6 +40,14 @@ public class FlowWorkOrderExt { private String businessData; /** + * 节点标题快照,JSON:{"taskKey": "解析后的标题"}。 + *

工单是「一个流程实例一行」,标题则是「实例 × 节点」级,故按 BPMN 节点Id(taskKey) 存成 JSON 承载多节点。 + * 由 FlowUserTaskListener 在任务创建时写入,查询时优先读取,取不到再回退实时解析。

+ */ + @TableField(value = "title_json") + private String titleJson; + + /** * 更新时间。 */ @TableField(value = "update_time") diff --git a/common/common-flow/src/main/java/apelet/common/flow/service/FlowApiService.java b/common/common-flow/src/main/java/apelet/common/flow/service/FlowApiService.java index 340cb47..6f2bd94 100644 --- a/common/common-flow/src/main/java/apelet/common/flow/service/FlowApiService.java +++ b/common/common-flow/src/main/java/apelet/common/flow/service/FlowApiService.java @@ -5,6 +5,7 @@ import apelet.common.flow.model.FlowEntry; import apelet.common.flow.model.FlowTaskComment; import apelet.common.flow.model.FlowTaskExt; import apelet.common.flow.model.FlowWorkOrder; +import apelet.common.flow.model.FlowWorkOrderExt; import apelet.common.flow.object.AnalyzedNode; import apelet.common.flow.object.FlowApiOption; import apelet.common.flow.vo.FlowTaskVo; @@ -658,4 +659,34 @@ public interface FlowApiService { * @return 解析后的标题,非在线表单工作流返回null。 */ String resolveTitle(String titleTemplate, String processInstanceId, FlowWorkOrder workOrder, FlowEntry flowEntry); + + /** + * 优先读取已生成的标题快照,取不到再回退 {@link #resolveTitle} 实时解析。 + *

快照在任务创建时由 {@link #buildAndSaveTaskTitle} 写入工单扩展的 titleJson,按 taskKey 定位。 + * 存量数据(改造前已跑的实例)没有快照,会自动走回退分支,因此无需刷数据。

+ * + * @param titleTemplate 节点配置的标题模板,回退时使用。 + * @param processInstanceId 流程实例Id。 + * @param workOrder 流程工单对象,为空时直接回退。 + * @param flowEntry 流程对象。 + * @param taskKey BPMN 节点Id,用于在快照中定位。 + * @param workOrderExt 工单扩展对象(调用方批量预载,避免逐行查询),可为空。 + * @return 解析后的标题。 + */ + String resolveTitleCached(String titleTemplate, String processInstanceId, FlowWorkOrder workOrder, + FlowEntry flowEntry, String taskKey, FlowWorkOrderExt workOrderExt); + + /** + * 生成指定节点的标题快照并落库。 + *

取自该节点配置的 title_template,用当时的流程变量与在线表单业务数据解析后写入 + * {@code zz_flow_work_order_ext.title_json}。非在线表单流程、工单为空、标题为空时不写入。

+ *

注意:首节点的 create 事件触发时工单尚未创建(工单在 flowApiService.start 返回后才建), + * 该场景下 taskKey 传空由本方法自行取当前活动任务节点,供工单创建后补写首节点标题使用。

+ * + * @param workOrder 流程工单对象。 + * @param processDefinitionId 流程定义Id,用于定位节点模板。 + * @param taskKey BPMN 节点Id,为空时取该实例当前的活动任务节点。 + * @return 写入的标题;未写入返回null。 + */ + String buildAndSaveTaskTitle(FlowWorkOrder workOrder, String processDefinitionId, String taskKey); } diff --git a/common/common-flow/src/main/java/apelet/common/flow/service/FlowWorkOrderService.java b/common/common-flow/src/main/java/apelet/common/flow/service/FlowWorkOrderService.java index d1dfd4e..7775f6d 100644 --- a/common/common-flow/src/main/java/apelet/common/flow/service/FlowWorkOrderService.java +++ b/common/common-flow/src/main/java/apelet/common/flow/service/FlowWorkOrderService.java @@ -160,6 +160,17 @@ public interface FlowWorkOrderService extends IBaseService List getFlowWorkOrderExtByWorkOrderIds(Set workOrderIds); /** + * 写入指定工单下某节点的标题快照。 + *

titleJson 是「实例 × 节点」的映射,写入时按 taskKey 合并(读-改-写), + * 保留其他节点已写入的条目,不能整串覆盖。扩展行不存在时自动创建。

+ * + * @param workOrderId 工单Id。 + * @param taskKey BPMN 节点Id(等价于运行时 taskDefinitionKey)。 + * @param title 解析后的标题。 + */ + void saveTaskTitle(Long workOrderId, String taskKey, String title); + + /** * 移除草稿工单,同时停止已经启动的流程实例。 * * @param flowWorkOrder 工单对象。 diff --git a/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowApiServiceImpl.java b/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowApiServiceImpl.java index 822d43b..e55235a 100644 --- a/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowApiServiceImpl.java +++ b/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowApiServiceImpl.java @@ -906,6 +906,11 @@ public class FlowApiServiceImpl implements FlowApiService { flowWorkOrderService.getInList("processInstanceId", procInstanceIdSet); Map workOrderMap = workOrderList.stream().collect(Collectors.toMap(FlowWorkOrder::getProcessInstanceId, c -> c)); + // 标题快照按工单批量预载(一次 IN 查询覆盖整页),避免逐行查扩展对象造成 N+1。 + Map workOrderExtMap = flowWorkOrderService + .getFlowWorkOrderExtByWorkOrderIds(workOrderList.stream() + .map(FlowWorkOrder::getWorkOrderId).filter(Objects::nonNull).collect(Collectors.toSet())) + .stream().collect(Collectors.toMap(FlowWorkOrderExt::getWorkOrderId, c -> c, (k1, k2) -> k1)); List allFlowTaskExtList = flowTaskExtService.getByProcessDefinitionId(processDefinitionIdSet); Map flowTaskExtMap = allFlowTaskExtList.stream().collect(Collectors.toMap(t -> t.getProcessDefinitionId() + ":" + t.getTaskId(), t -> t, (k1, k2) -> k1)); Map multiInstanceTaskMap = new HashMap<>(); @@ -938,7 +943,9 @@ public class FlowApiServiceImpl implements FlowApiService { FlowTaskExt flowTaskExt = flowTaskExtMap.get(task.getProcessDefinitionId() + ":" + task.getTaskDefinitionKey()); FlowEntry flowEntry = flowEntryMap.get(flowEntryPublishMap.get(task.getProcessDefinitionId()).getEntryId()); if (flowTaskExt != null && flowEntry != null) { - flowTaskVo.setTitle(this.resolveTitle(flowTaskExt.getTitleTemplate(), task.getProcessInstanceId(), flowWorkOrder, flowEntry)); + flowTaskVo.setTitle(this.resolveTitleCached(flowTaskExt.getTitleTemplate(), + task.getProcessInstanceId(), flowWorkOrder, flowEntry, task.getTaskDefinitionKey(), + flowWorkOrder == null ? null : workOrderExtMap.get(flowWorkOrder.getWorkOrderId()))); } flowTaskVoList.add(flowTaskVo); } @@ -2647,6 +2654,10 @@ public class FlowApiServiceImpl implements FlowApiService { String result = titleTemplate; // 1. Flowable 持久化变量: ${initiator}, ${startUserName}, ${submitUser} List varNames = ReUtil.findAll("\\$\\{(\\w+)\\}", result, 1); + // 业务数据只查一次。原来写在占位符循环内部,N 个业务占位符就调 N 次 getBusinessData(重操作:查库 + + // 字典翻译 + 附件列填充 + 关联重排),列表接口再逐行调用,会放大成 M 行 × N 占位符次查询。 + JSONObject bizData = null; + boolean bizDataLoaded = false; for (String varName : varNames) { String value = null; if (StrUtil.isNotBlank(processInstanceId)) { @@ -2667,10 +2678,13 @@ public class FlowApiServiceImpl implements FlowApiService { } if (value == null) { // 2. 业务数据: ${number}, ${amount} 等在线表单字段。 - String bizData = flowCustomExtFactory.getOnlineBusinessDataExtHelper().getBusinessData(workOrder, flowEntry); - if (StrUtil.isNotBlank(bizData)) { - JSONObject biz = JSON.parseObject(bizData); - value = biz.getString(varName); + if (!bizDataLoaded) { + bizDataLoaded = true; + String bizJson = flowCustomExtFactory.getOnlineBusinessDataExtHelper().getBusinessData(workOrder, flowEntry); + bizData = StrUtil.isBlank(bizJson) ? null : JSON.parseObject(bizJson); + } + if (bizData != null) { + value = bizData.getString(varName); } } if (value != null) { @@ -2680,6 +2694,79 @@ public class FlowApiServiceImpl implements FlowApiService { return result; } + @Override + public String resolveTitleCached(String titleTemplate, String processInstanceId, FlowWorkOrder workOrder, + FlowEntry flowEntry, String taskKey, FlowWorkOrderExt workOrderExt) { + if (workOrder != null && workOrderExt != null && StrUtil.isNotBlank(taskKey) + && StrUtil.isNotBlank(workOrderExt.getTitleJson())) { + try { + String cached = JSON.parseObject(workOrderExt.getTitleJson()).getString(taskKey); + if (StrUtil.isNotBlank(cached)) { + return cached; + } + } catch (Exception e) { + // 快照解析失败不能影响列表整体,降级为实时解析。 + log.warn("标题快照解析失败,降级为实时解析。workOrderId [{}], taskKey [{}]", + workOrder.getWorkOrderId(), taskKey, e); + } + } + return this.resolveTitle(titleTemplate, processInstanceId, workOrder, flowEntry); + } + + @Override + public String buildAndSaveTaskTitle(FlowWorkOrder workOrder, String processDefinitionId, String taskKey) { + if (workOrder == null || StrUtil.isBlank(workOrder.getProcessInstanceId())) { + return null; + } + String nodeKey = taskKey; + if (StrUtil.isBlank(nodeKey)) { + // 首节点补写场景:create 事件触发时工单还没创建,走到这里工单已建,取当前活动任务节点即可。 + Task activeTask = taskService.createTaskQuery() + .processInstanceId(workOrder.getProcessInstanceId()).active().singleResult(); + if (activeTask == null) { + return null; + } + nodeKey = activeTask.getTaskDefinitionKey(); + } + FlowEntry flowEntry = StrUtil.isBlank(workOrder.getProcessDefinitionKey()) ? null + : flowEntryService.getFlowEntryFromCache(workOrder.getProcessDefinitionKey()); + if (flowEntry == null) { + return null; + } + FlowTaskExt flowTaskExt = flowTaskExtService.getByProcessDefinitionIdAndTaskId(processDefinitionId, nodeKey); + String titleTemplate = flowTaskExt == null ? null : flowTaskExt.getTitleTemplate(); + titleTemplate = this.replaceSubmitUserWhenAbsent(titleTemplate, workOrder.getProcessInstanceId()); + String title = this.resolveTitle(titleTemplate, workOrder.getProcessInstanceId(), workOrder, flowEntry); + if (StrUtil.isBlank(title)) { + return null; + } + flowWorkOrderService.saveTaskTitle(workOrder.getWorkOrderId(), nodeKey, title); + return title; + } + + /** + * 首节点生成标题时还没发生过任何任务完成,${submitUser} 尚未写入(它由 completeTask 设置), + * 直接解析会留下字面量。首节点的"提交人"语义上就是发起人,故此时把模板里的 ${submitUser} 换成 ${initiator}。 + * 第 2 个节点起上一个节点的 complete 已写好 submitUser,原样返回。 + */ + private String replaceSubmitUserWhenAbsent(String titleTemplate, String processInstanceId) { + if (StrUtil.isBlank(titleTemplate) || StrUtil.isBlank(processInstanceId) + || !titleTemplate.contains("${" + FlowConstant.SUBMIT_USER_VAR + "}")) { + return titleTemplate; + } + Object submitUser = null; + try { + submitUser = runtimeService.getVariable(processInstanceId, FlowConstant.SUBMIT_USER_VAR); + } catch (Exception e) { + submitUser = this.getHistoricProcessInstanceVariable(processInstanceId, FlowConstant.SUBMIT_USER_VAR); + } + if (submitUser != null && StrUtil.isNotBlank(submitUser.toString())) { + return titleTemplate; + } + return titleTemplate.replace("${" + FlowConstant.SUBMIT_USER_VAR + "}", + "${" + FlowConstant.PROC_INSTANCE_INITIATOR_VAR + "}"); + } + private String buildDefaultTitle(FlowEntry flowEntry) { String tableName = null; if (flowEntry != null && flowEntry.getPageId() != null) { diff --git a/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowWorkOrderServiceImpl.java b/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowWorkOrderServiceImpl.java index 7bb29b1..a99f702 100644 --- a/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowWorkOrderServiceImpl.java +++ b/common/common-flow/src/main/java/apelet/common/flow/service/impl/FlowWorkOrderServiceImpl.java @@ -113,6 +113,12 @@ public class FlowWorkOrderServiceImpl extends BaseService i // 并再次提交当前的工单数据。 flowWorkOrderMapper.insert(flowWorkOrder); } + // 首节点的 create 事件在 flowApiService.start 内部就触发了,那时工单还没建、拿不到 workOrderId, + // 所以首节点标题在这里补写一次;第 2..N 个节点由 FlowUserTaskListener 在任务创建时写入。 + // 草稿路径 businessKey 为空、主表还没有数据,算不出业务字段,跳过(其首节点标题走读取侧回退)。 + if (dataId != null) { + flowApiService.buildAndSaveTaskTitle(flowWorkOrder, flowWorkOrder.getProcessDefinitionId(), null); + } return flowWorkOrder; } @@ -304,12 +310,47 @@ public class FlowWorkOrderServiceImpl extends BaseService i @Override public List getFlowWorkOrderExtByWorkOrderIds(Set workOrderIds) { + // 空集合会产生非法的 IN () 语句,这里直接短路返回。 + if (CollUtil.isEmpty(workOrderIds)) { + return new ArrayList<>(); + } return flowWorkOrderExtMapper.selectList( new LambdaQueryWrapper().in(FlowWorkOrderExt::getWorkOrderId, workOrderIds)); } @Transactional(rollbackFor = Exception.class) @Override + public void saveTaskTitle(Long workOrderId, String taskKey, String title) { + if (workOrderId == null || StrUtil.isBlank(taskKey) || StrUtil.isBlank(title)) { + return; + } + FlowWorkOrderExt existExt = this.getFlowWorkOrderExtByWorkOrderId(workOrderId); + JSONObject titleJson = existExt == null || StrUtil.isBlank(existExt.getTitleJson()) + ? new JSONObject() : JSON.parseObject(existExt.getTitleJson()); + // 同一实例的每个节点各写各的条目,这里必须是合并写入,整串覆盖会抹掉其他节点已写的标题 + titleJson.put(taskKey, title); + if (existExt == null) { + // 工单扩展行并非随工单必建(只有草稿路径会建),首次写入标题时需要补一行 + FlowWorkOrder flowWorkOrder = flowWorkOrderMapper.selectById(workOrderId); + FlowWorkOrderExt newExt = flowWorkOrder == null + ? new FlowWorkOrderExt() : BeanUtil.copyProperties(flowWorkOrder, FlowWorkOrderExt.class); + newExt.setId(idGenerator.nextLongId()); + newExt.setWorkOrderId(workOrderId); + newExt.setTitleJson(titleJson.toJSONString()); + flowWorkOrderExtMapper.insert(newExt); + return; + } + // 只更新 title_json,避免把 draft_data / business_data 一并覆盖 + FlowWorkOrderExt updateExt = new FlowWorkOrderExt(); + updateExt.setId(existExt.getId()); + updateExt.setTitleJson(titleJson.toJSONString()); + updateExt.setUpdateTime(new Date()); + updateExt.setUpdateUserId(TokenData.takeFromRequest() == null ? null : TokenData.takeFromRequest().getUserId()); + flowWorkOrderExtMapper.updateById(updateExt); + } + + @Transactional(rollbackFor = Exception.class) + @Override public CallResult removeDraft(FlowWorkOrder flowWorkOrder) { CallResult r = flowApiService.stopProcessInstance(flowWorkOrder.getProcessInstanceId(), "撤销草稿", true); if (!r.isSuccess()) {