实现对空间重命名,需要考虑到以下问题:
这操作可能需要大量的时间,所以我们需要显示进度。这意味着我们不能阻塞触发操作的HTTP请求。换句话说,操作应该是异步的。
在我们开始实现之前,我们需要设计重命名API。实现异步任务的主要方法有2种:
第一个选项(push)是很好的,但它需要触发任务代码和执行任务代码之间的双向连接。这是不符合(标准)的HTTP协议,服务端(通常)是不会把数据推送到客户端。客户端是可以从服务器拉数据。因此,我们要使用第二个选项。
## Start the task. #set (taskId = services.space.rename(spaceReference, newSpaceName)) ... ## Pull the task status. #set (taskStatus = services.space.getRenameStatus(
查看Job Module了解如何实现此API。
request表示该任务的输入。这包括:
每一个请求都有一个用来访问任务状态的标识符。
public class RenameRequest extends org.xwiki.job.AbstractRequest { private static final String PROPERTY_SPACE_REFERENCE = "spaceReference"; private static final String PROPERTY_NEW_SPACE_NAME = "newSpaceName"; private static final String PROPERTY_CHECK_RIGHTS = "checkrights"; private static final String PROPERTY_USER_REFERENCE = "user.reference"; public SpaceReference getSpaceReference() { return getProperty(PROPERTY_SPACE_REFERENCE); } public void setSpaceReference(SpaceReference spaceReference) { setProperty(PROPERTY_SPACE_REFERENCE, spaceReference); } public String getNewSpaceName() { return getProperty(PROPERTY_NEW_SPACE_NAME); } public void setNewSpaceName(String newSpaceName) { setProperty(PROPERTY_NEW_SPACE_NAME, newSpaceName); } public boolean isCheckRights() { return getProperty(PROPERTY_CHECK_RIGHTS, true); } public void setCheckRights(boolean checkRights) { setProperty(PROPERTY_CHECK_RIGHTS, checkRights); } public DocumentReference getUserReference() { return getProperty(PROPERTY_USER_REFERENCE); } public void setUserReference(DocumentReference userReference) { setProperty(PROPERTY_USER_REFERENCE, userReference); } }
正如我们所提到的,在作业执行过程中,作业可以通过asking questions进行互动。例如,如果已经有一个空间与新的空间名称重复,那么我们就必须决定是否:
如果我们决定合并两个空间,则有可能在两个空间有一样名字的文档,在这种情况下,我们必须决定是否覆盖目标文档。
为了让这个例子简单点,我们始终合并这两个空间,但我们会要求用户确认是否覆盖。
public class OverwriteQuestion { private final DocumentReference source; private final DocumentReference destination; private boolean overwrite = true; private boolean askAgain = true; public OverwriteQuestion(DocumentReference source, DocumentReference destination) { this.source = source; this.destination = destination; } public EntityReference getSource() { return source; } public EntityReference getDestination() { return destination; } public boolean isOverwrite() { return overwrite; } public void setOverwrite(boolean overwrite) { this.overwrite = overwrite; } public boolean isAskAgain() { return askAgain; } public void setAskAgain(boolean askAgain) { this.askAgain = askAgain; } }
提供作业状态,默认情况下,访问:
大多数时候,你并不需要扩展作业模块提供的DefaultJobStatus,除非你想存储:
请注意,请求和作业状态必须是可序列化,所以要小心你在自定义作业状态存储什么样信息。例如,对于任务输出,可能在输出存储一个引用,路径或URL更好而不是存储输出本身。
作业状态也是job沟通通道:
public class RenameJobStatus extends DefaultJobStatus<RenameRequest> { private boolean canceled; private List<DocumentReference> renamedDocumentReferences = new ArrayList<>(); public RenameJobStatus(RenameRequest request, ObservationManager observationManager, LoggerManager loggerManager, JobStatus parentJobStatus) { super(request, observationManager, loggerManager, parentJobStatus); } public void cancel() { this.canceled = true; } public boolean isCanceled() { return this.canceled; } public List<DocumentReference> getRenamedDocumentReferences() { return this.renamedDocumentReferences; } }
现在,我们需要实现一个ScriptService,使用Velocity触发重命名,并得到重命名状态。
@Component @Named(SpaceScriptService.ROLE_HINT) @Singleton public class SpaceScriptService implements ScriptService { public static final String ROLE_HINT = "space"; public static final String RENAME = "rename"; @Inject private JobExecutor jobExecutor; @Inject private JobStatusStore jobStatusStore; @Inject private DocumentAccessBridge documentAccessBridge; public String rename(SpaceReference spaceReference, String newSpaceName) { setError(null); RenameRequest renameRequest = createRenameRequest(spaceReference, newSpaceName); try { this.jobExecutor.execute(RENAME, renameRequest); List<String> renameId = renameRequest.getId(); return renameId.get(renameId.size() - 1); } catch (Exception e) { setError(e); return null; } } public RenameJobStatus getRenameStatus(String renameJobId) { return (RenameJobStatus) this.jobStatusStore.getJobStatus(getJobId(renameJobId)); } private RenameRequest createRenameRequest(SpaceReference spaceReference, String newSpaceName) { RenameRequest renameRequest = new RenameRequest(); renameRequest.setId(getNewJobId()); renameRequest.setSpaceReference(spaceReference); renameRequest.setNewSpaceName(newSpaceName); renameRequest.setInteractive(true); renameRequest.setCheckRights(true); renameRequest.setUserReference(this.documentAccessBridge.getCurrentUserReference()); return renameRequest; } private List<String> getNewJobId() { return getJobId(UUID.randomUUID().toString()); } private List<String> getJobId(String suffix) { return Arrays.asList(ROLE_HINT, RENAME, suffix); } }
Jobs是个组件。让我们来看看我们如何能够实现它们。
@Component @Named(SpaceScriptService.RENAME) public class RenameJob extends AbstractJob<RenameRequest, RenameJobStatus> implements GroupedJob { @Inject private AuthorizationManager authorization; @Inject private DocumentAccessBridge documentAccessBridge; private Boolean overwriteAll; @Override public String getType() { return SpaceScriptService.RENAME; } @Override public JobGroupPath getGroupPath() { String wiki = this.request.getSpaceReference().getWikiReference().getName(); return new JobGroupPath(Arrays.asList(SpaceScriptService.RENAME, wiki)); } @Override protected void runInternal() throws Exception { List<DocumentReference> documentReferences = getDocumentReferences(this.request.getSpaceReference()); this.progressManager.pushLevelProgress(documentReferences.size(), this); try { for (DocumentReference documentReference : documentReferences) { if (this.status.isCanceled()) { break; } else { this.progressManager.startStep(this); if (hasAccess(Right.DELETE, documentReference)) { move(documentReference, this.request.getNewSpaceName()); this.status.getRenamedDocumentReferences().add(documentReference); this.logger.info("Document [{}] has been moved to [{}].", documentReference, this.request.getNewSpaceName()); } } } } finally { this.progressManager.popLevelProgress(this); } } private boolean hasAccess(Right right, EntityReference reference) { return !this.request.isCheckRights() || this.authorization.hasAccess(right, this.request.getUserReference(), reference); } private void move(DocumentReference documentReference, String newSpaceName) { SpaceReference newSpaceReference = new SpaceReference(newSpaceName, documentReference.getWikiReference()); DocumentReference newDocumentReference = documentReference.replaceParent(documentReference.getLastSpaceReference(), newSpaceReference); if (!this.documentAccessBridge.exists(newDocumentReference) || confirmOverwrite(documentReference, newDocumentReference)) { move(documentReference, newDocumentReference); } } private boolean confirmOverwrite(DocumentReference source, DocumentReference destination) { if (this.overwriteAll == null) { OverwriteQuestion question = new OverwriteQuestion(source, destination); try { this.status.ask(question); if (!question.isAskAgain()) { // Use the same answer for the following overwrite questions. this.overwriteAll = question.isOverwrite(); } return question.isOverwrite(); } catch (InterruptedException e) { this.logger.warn("Overwrite question has been interrupted."); return false; } } else { return this.overwriteAll; } } }
我们需要从JavaScript能够触发重命名操作和远程定时获得状态更新。这意味着重命名API应该可以通过一些URL访问:
{{velocity}} #if (request.action == 'rename') #set (spaceReference = services.model.resolveSpace(request.spaceReference)) #set (renameJobId = services.space.rename(spaceReference, request.newSpaceName)) response.sendRedirect(doc.getURL('get', escapetool.url({ 'outputSyntax': 'plain', 'jobId': renameJobId }))) #elseif (request.action == 'continue') #set (renameJobStatus = services.space.getRenameStatus(request.jobId)) #set (overwrite = request.overwrite == 'true') #set (discard = renameJobStatus.question.setOverwrite(overwrite)) #set (discard = renameJobStatus..answered()) #elseif (request.action == 'cancel') #set (renameJobStatus = services.space.getRenameStatus(request.jobId)) #set (discard = renameJobStatus.cancel()) response.sendRedirect(doc.getURL('get', escapetool.url({ 'outputSyntax': 'plain', 'jobId': renameJobId }))) #elseif (request.data == 'jobStatus') #set (renameJobStatus = services.space.getRenameStatus(request.jobId)) #buildRenameStatusJSON(renameJobStatus) response.setContentType('application/json') jsontool.serialize(
在客户端,JavaScript代码负责:
var onStatusUpdate = function(status) { updateProgressBar(status); if (status.state == 'WAITING') { // Display the question to the user. displayQuestion(status); } else if (status.state != 'FINISHED') { // Pull task status update. setTimeout(function() { requestStatusUpdate(status.request.id).success(onStatusUpdate); }, 1000); } }; // Trigger the rename task. rename(parameters).success(onStatusUpdate); // Continue the rename after the user answers the question. continueRename(parameters).success(onStatusUpdate); // Cancel the rename. cancelRename(parameters).success(onStatusUpdate);
