/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.refactoring.internal.job;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import org.xwiki.component.annotation.Component;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.model.reference.SpaceReference;
import org.xwiki.refactoring.internal.LinkRefactoring;
import org.xwiki.refactoring.job.EntityJobStatus;
import org.xwiki.refactoring.job.MoveRequest;
import org.xwiki.refactoring.job.OverwriteQuestion;
import org.xwiki.refactoring.job.RefactoringJobs;
import org.xwiki.security.authorization.Right;
import org.xwiki.wiki.descriptor.WikiDescriptorManager;
import org.xwiki.wiki.manager.WikiManagerException;
/**
* A job that can move entities to a new parent within the hierarchy.
*
* @version $Id: 95ad693e7a4f34337abaffafa02c8f0ac74bd62b $
* @since 7.2M1
*/
@Component
@Named(RefactoringJobs.MOVE)
public class MoveJob extends AbstractEntityJob<MoveRequest, EntityJobStatus<MoveRequest>>
{
/**
* Specifies whether all entities with the same name are to be overwritten on not. When {@code true} all entities
* with the same name are overwritten. When {@code false} all entities with the same name are skipped. If
* {@code null} then a question is asked for each entity.
*/
private Boolean overwriteAll;
/**
* The component used to refactor document links after a document is rename or moved.
*/
@Inject
private LinkRefactoring linkRefactoring;
@Inject
private WikiDescriptorManager wikiDescriptorManager;
@Override
public String getType()
{
return RefactoringJobs.MOVE;
}
@Override
protected void runInternal() throws Exception
{
if (this.request.getDestination() != null) {
super.runInternal();
}
}
@Override
protected void process(EntityReference source)
{
// Perform generic checks that don't depend on the source/destination type.
EntityReference destination = this.request.getDestination();
if (isDescendantOrSelf(destination, source)) {
this.logger.error("Cannot make [{}] a descendant of itself.", source);
return;
}
// Dispatch the move operation based on the source entity type.
switch (source.getType()) {
case DOCUMENT:
process(new DocumentReference(source), destination);
break;
case SPACE:
process(new SpaceReference(source), destination);
break;
default:
this.logger.error("Unsupported source entity type [{}].", source.getType());
}
}
private boolean isDescendantOrSelf(EntityReference alice, EntityReference bob)
{
EntityReference parent = alice;
while (parent != null && !parent.equals(bob)) {
parent = parent.getParent();
}
return parent != null;
}
protected void process(DocumentReference source, EntityReference destination)
{
if (this.request.isDeep() && isSpaceHomeReference(source)) {
process(source.getLastSpaceReference(), destination);
} else if (destination.getType() == EntityType.SPACE) {
maybeMove(source, new DocumentReference(source.getName(), new SpaceReference(destination)));
} else if (destination.getType() == EntityType.DOCUMENT
&& isSpaceHomeReference(new DocumentReference(destination))) {
maybeMove(source, new DocumentReference(source.getName(), new SpaceReference(destination.getParent())));
} else {
this.logger.error("Unsupported destination entity type [{}] for a document.", destination.getType());
}
}
protected void process(SpaceReference source, EntityReference destination)
{
if (destination.getType() == EntityType.SPACE || destination.getType() == EntityType.WIKI) {
process(source, new SpaceReference(source.getName(), destination));
} else if (destination.getType() == EntityType.DOCUMENT
&& isSpaceHomeReference(new DocumentReference(destination))) {
process(source, new SpaceReference(source.getName(), destination.getParent()));
} else {
this.logger.error("Unsupported destination entity type [{}] for a space.", destination.getType());
}
}
protected void process(final SpaceReference source, final SpaceReference destination)
{
visitDocuments(source, new Visitor<DocumentReference>()
{
@Override
public void visit(DocumentReference oldChildReference)
{
DocumentReference newChildReference = oldChildReference.replaceParent(source, destination);
maybeMove(oldChildReference, newChildReference);
}
});
}
protected void maybeMove(DocumentReference oldReference, DocumentReference newReference)
{
// Perform checks that are specific to the document source/destination type.
if (!this.modelBridge.exists(oldReference)) {
this.logger.warn("Skipping [{}] because it doesn't exist.", oldReference);
} else if (this.request.isDeleteSource() && !hasAccess(Right.DELETE, oldReference)) {
// The move operation is implemented as Copy + Delete.
this.logger.error("You are not allowed to delete [{}].", oldReference);
} else if (!hasAccess(Right.VIEW, newReference) || !hasAccess(Right.EDIT, newReference)
|| (this.modelBridge.exists(newReference) && !hasAccess(Right.DELETE, newReference))) {
this.logger.error("You don't have sufficient permissions over the destination document [{}].",
newReference);
} else {
move(oldReference, newReference);
}
}
private void move(DocumentReference oldReference, DocumentReference newReference)
{
this.progressManager.pushLevelProgress(7, this);
try {
// Step 1: Delete the destination document if needed.
this.progressManager.startStep(this);
if (this.modelBridge.exists(newReference)) {
if (this.request.isInteractive() && !confirmOverwrite(oldReference, newReference)) {
this.logger.warn(
"Skipping [{}] because [{}] already exists and the user doesn't want to overwrite it.",
oldReference, newReference);
return;
} else if (!this.modelBridge.delete(newReference)) {
return;
}
}
this.progressManager.endStep(this);
// Step 2: Copy the source document to the destination.
this.progressManager.startStep(this);
if (!this.modelBridge.copy(oldReference, newReference)) {
return;
}
this.progressManager.endStep(this);
// Step 3: Update the destination document based on the source document parameters.
this.progressManager.startStep(this);
this.modelBridge.update(newReference, this.request.getEntityParameters(oldReference));
this.progressManager.endStep(this);
// Step 4 + 5: Update other documents that might be affected by this move.
updateDocuments(oldReference, newReference);
// Step 6: Delete the source document.
this.progressManager.startStep(this);
if (this.request.isDeleteSource()) {
this.modelBridge.delete(oldReference);
}
this.progressManager.endStep(this);
// Step 7: Create an automatic redirect.
this.progressManager.startStep(this);
if (this.request.isDeleteSource() && this.request.isAutoRedirect()) {
this.modelBridge.createRedirect(oldReference, newReference);
}
} finally {
this.progressManager.popLevelProgress(this);
}
}
private void updateDocuments(DocumentReference oldReference, DocumentReference newReference)
{
// Step 3: Update the links.
this.progressManager.startStep(this);
if (this.request.isUpdateLinks()) {
updateLinks(oldReference, newReference);
}
this.progressManager.endStep(this);
// Step 4: (legacy) Preserve existing parent-child relationships by updating the parent field of documents
// having the moved document as parent.
this.progressManager.startStep(this);
if (this.request.isUpdateParentField()) {
this.modelBridge.updateParentField(oldReference, newReference);
}
this.progressManager.endStep(this);
}
private boolean confirmOverwrite(EntityReference source, EntityReference 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;
}
}
private void updateLinks(DocumentReference oldReference, DocumentReference newReference)
{
this.progressManager.pushLevelProgress(2, this);
try {
// Step 1: Update the links that target the old reference to point to the new reference.
this.progressManager.startStep(this);
if (this.request.isDeleteSource()) {
updateBackLinks(oldReference, newReference);
}
this.progressManager.endStep(this);
// Step 2: Update the relative links from the document content.
this.progressManager.startStep(this);
this.linkRefactoring.updateRelativeLinks(oldReference, newReference);
} finally {
this.progressManager.popLevelProgress(this);
}
}
private void updateBackLinks(DocumentReference oldReference, DocumentReference newReference)
{
Collection<String> wikiIds = Collections.singleton(oldReference.getWikiReference().getName());
if (this.request.isUpdateLinksOnFarm()) {
try {
wikiIds = this.wikiDescriptorManager.getAllIds();
} catch (WikiManagerException e) {
this.logger.error("Failed to retrieve the list of wikis.", e);
}
}
boolean popLevelProgress = false;
try {
if (wikiIds.size() > 0) {
this.progressManager.pushLevelProgress(wikiIds.size(), this);
popLevelProgress = true;
}
for (String wikiId : wikiIds) {
this.progressManager.startStep(this);
updateBackLinks(oldReference, newReference, wikiId);
this.progressManager.endStep(this);
}
} finally {
if (popLevelProgress) {
this.progressManager.popLevelProgress(this);
}
}
}
private void updateBackLinks(DocumentReference oldReference, DocumentReference newReference, String wikiId)
{
this.logger.info("Updating the back-links for document [{}] in wiki [{}].", oldReference, wikiId);
List<DocumentReference> backlinkDocumentReferences =
this.modelBridge.getBackLinkedReferences(oldReference, wikiId);
this.progressManager.pushLevelProgress(backlinkDocumentReferences.size(), this);
try {
for (DocumentReference backlinkDocumentReference : backlinkDocumentReferences) {
this.progressManager.startStep(this);
if (hasAccess(Right.EDIT, backlinkDocumentReference)) {
this.linkRefactoring.renameLinks(backlinkDocumentReference, oldReference, newReference);
}
this.progressManager.endStep(this);
}
} finally {
this.progressManager.popLevelProgress(this);
}
}
@Override
protected EntityReference getCommonParent()
{
if (this.request.isUpdateLinksOnFarm()) {
return null;
} else {
List<EntityReference> entityReferences = new LinkedList<>(this.request.getEntityReferences());
entityReferences.add(this.request.getDestination());
return getCommonParent(entityReferences);
}
}
}