/*
* 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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.context.Execution;
import org.xwiki.job.event.status.JobProgressManager;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.model.reference.EntityReferenceProvider;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.model.reference.LocalDocumentReference;
import org.xwiki.model.reference.SpaceReference;
import org.xwiki.query.Query;
import org.xwiki.query.QueryManager;
import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.api.DeletedDocument;
import com.xpn.xwiki.doc.XWikiDeletedDocument;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.internal.parentchild.ParentChildConfiguration;
/**
* Default implementation of {@link ModelBridge} based on the old XWiki model.
*
* @version $Id: e6238a7321ce2db80d0144994a419478b7bea004 $
* @since 7.4M2
*/
@Component
@Singleton
public class DefaultModelBridge implements ModelBridge
{
/**
* Regular expression used to match the special characters supported by the like HQL operator (plus the escaping
* character).
*/
private static final Pattern LIKE_SPECIAL_CHARS = Pattern.compile("([%_/])");
/**
* The reference to the type of object used to create an automatic redirect when renaming or moving a document.
*/
private static final LocalDocumentReference REDIRECT_CLASS_REFERENCE =
new LocalDocumentReference(XWiki.SYSTEM_SPACE, "RedirectClass");
@Inject
private Logger logger;
/**
* Used to perform the low level operations on entities.
*/
@Inject
private Provider<XWikiContext> xcontextProvider;
/**
* Used to query the child documents.
*/
@Inject
private QueryManager queryManager;
/**
* Used to serialize a space reference in order to query the child documents.
*/
@Inject
@Named("local")
private EntityReferenceSerializer<String> localEntityReferenceSerializer;
/**
* Used to serialize the redirect location.
*
* @see #createRedirect(DocumentReference, DocumentReference)
*/
@Inject
private EntityReferenceSerializer<String> defaultEntityReferenceSerializer;
/**
* Used to resolve the references of child documents.
*/
@Inject
@Named("explicit")
private DocumentReferenceResolver<String> explicitDocumentReferenceResolver;
@Inject
private JobProgressManager progressManager;
@Inject
private ParentChildConfiguration parentChildConfiguration;
@Inject
private EntityReferenceProvider entityReferenceProvider;
@Inject
private Execution execution;
@Override
public boolean create(DocumentReference documentReference)
{
XWikiContext xcontext = this.xcontextProvider.get();
try {
XWikiDocument newDocument = xcontext.getWiki().getDocument(documentReference, xcontext);
xcontext.getWiki().saveDocument(newDocument, xcontext);
this.logger.info("Document [{}] has been created.", documentReference);
return true;
} catch (Exception e) {
this.logger.error("Failed to create document [{}].", documentReference, e);
return false;
}
}
@Override
public boolean copy(DocumentReference source, DocumentReference destination)
{
XWikiContext xcontext = this.xcontextProvider.get();
try {
String language = source.getLocale() != null ? source.getLocale().toString() : null;
boolean result =
xcontext.getWiki().copyDocument(source, destination, language, false, true, true, xcontext);
if (result) {
this.logger.info("Document [{}] has been copied to [{}].", source, destination);
} else {
this.logger.warn(
"Cannot fully copy [{}] to [{}] because an orphan translation" + " exists at the destination.",
source, destination);
}
return result;
} catch (Exception e) {
this.logger.error("Failed to copy [{}] to [{}].", source, destination, e);
return false;
}
}
@Override
public boolean delete(DocumentReference reference)
{
XWikiContext xcontext = this.xcontextProvider.get();
try {
XWikiDocument document = xcontext.getWiki().getDocument(reference, xcontext);
if (document.getTranslation() == 1) {
xcontext.getWiki().deleteDocument(document, xcontext);
this.logger.info("Document [{}] has been deleted.", reference);
} else {
xcontext.getWiki().deleteAllDocuments(document, xcontext);
this.logger.info("Document [{}] has been deleted with all its translations.", reference);
}
return true;
} catch (Exception e) {
this.logger.error("Failed to delete document [{}].", reference, e);
return false;
}
}
@Override
public boolean removeLock(DocumentReference reference)
{
XWikiContext xcontext = this.xcontextProvider.get();
try {
XWikiDocument document = xcontext.getWiki().getDocument(reference, xcontext);
if (document.getLock(xcontext) != null) {
document.removeLock(xcontext);
this.logger.info("Document [{}] has been unlocked.", reference);
}
return true;
} catch (Exception e) {
// Just warn, since it's a recoverable situation.
this.logger.warn("Failed to unlock document [{}].", reference, e);
return false;
}
}
@Override
public void createRedirect(DocumentReference oldReference, DocumentReference newReference)
{
XWikiContext xcontext = this.xcontextProvider.get();
DocumentReference redirectClassReference =
new DocumentReference(REDIRECT_CLASS_REFERENCE, oldReference.getWikiReference());
if (xcontext.getWiki().exists(redirectClassReference, xcontext)) {
try {
XWikiDocument oldDocument = xcontext.getWiki().getDocument(oldReference, xcontext);
int number = oldDocument.createXObject(redirectClassReference, xcontext);
String location = this.defaultEntityReferenceSerializer.serialize(newReference);
oldDocument.getXObject(redirectClassReference, number).setStringValue("location", location);
oldDocument.setHidden(true);
xcontext.getWiki().saveDocument(oldDocument, "Create automatic redirect.", xcontext);
this.logger.info("Created automatic redirect from [{}] to [{}].", oldReference, newReference);
} catch (XWikiException e) {
this.logger.error("Failed to create automatic redirect from [{}] to [{}].", oldReference, newReference,
e);
}
} else {
this.logger.warn("We can't create an automatic redirect from [{}] to [{}] because [{}] is missing.",
oldReference, newReference, redirectClassReference);
}
}
@Override
public boolean exists(DocumentReference reference)
{
XWikiContext xcontext = this.xcontextProvider.get();
return xcontext.getWiki().exists(reference, xcontext);
}
@Override
public List<DocumentReference> getBackLinkedReferences(DocumentReference documentReference, String wikiId)
{
XWikiContext xcontext = this.xcontextProvider.get();
String previousWikiId = xcontext.getWikiId();
try {
xcontext.setWikiId(wikiId);
return xcontext.getWiki().getDocument(documentReference, xcontext).getBackLinkedReferences(xcontext);
} catch (XWikiException e) {
this.logger.error("Failed to retrieve the back-links for document [{}] on wiki [{}].", documentReference,
wikiId, e);
return Collections.emptyList();
} finally {
xcontext.setWikiId(previousWikiId);
}
}
@Override
public List<DocumentReference> getDocumentReferences(SpaceReference spaceReference)
{
try {
// At the moment we don't have a way to retrieve only the direct children so we select all the descendants.
// This means we select all the documents from the specified space and from all the nested spaces.
String statement = "select distinct(doc.fullName) from XWikiDocument as doc "
+ "where doc.space = :space or doc.space like :spacePrefix escape '/'";
Query query = this.queryManager.createQuery(statement, Query.HQL);
query.setWiki(spaceReference.getWikiReference().getName());
String localSpaceReference = this.localEntityReferenceSerializer.serialize(spaceReference);
query.bindValue("space", localSpaceReference);
String spacePrefix = LIKE_SPECIAL_CHARS.matcher(localSpaceReference).replaceAll("/$1");
query.bindValue("spacePrefix", spacePrefix + ".%");
List<DocumentReference> descendants = new ArrayList<>();
for (Object fullName : query.execute()) {
descendants.add(this.explicitDocumentReferenceResolver.resolve((String) fullName, spaceReference));
}
return descendants;
} catch (Exception e) {
this.logger.error("Failed to retrieve the documents from [{}].", spaceReference, e);
return Collections.emptyList();
}
}
@Override
public boolean updateParentField(final DocumentReference oldParentReference,
final DocumentReference newParentReference)
{
XWikiContext context = xcontextProvider.get();
XWiki wiki = context.getWiki();
boolean popLevelProgress = false;
try {
// Note: This operation could have been done in Hibernate (using the Store API) in one single update query.
// However, due to XWiki's document cache, it`s better in the end to use the Document API and update each
// child document individually.
XWikiDocument oldParentDocument = wiki.getDocument(oldParentReference, context);
List<DocumentReference> childReferences = oldParentDocument.getChildrenReferences(context);
if (childReferences.size() > 0) {
this.progressManager.pushLevelProgress(childReferences.size(), this);
popLevelProgress = true;
}
for (DocumentReference childReference : childReferences) {
this.progressManager.startStep(this);
XWikiDocument childDocument = wiki.getDocument(childReference, context);
childDocument.setParentReference(newParentReference);
wiki.saveDocument(childDocument, "Updated parent field.", true, context);
this.progressManager.endStep(this);
}
if (childReferences.size() > 0) {
this.logger.info("Document parent fields updated from [{}] to [{}] for [{}] documents.",
oldParentReference, newParentReference, childReferences.size());
}
} catch (Exception e) {
this.logger.error("Failed to update the document parent fields from [{}] to [{}].", oldParentReference,
newParentReference, e);
return false;
} finally {
if (popLevelProgress) {
this.progressManager.popLevelProgress(this);
}
}
return true;
}
@Override
public DocumentReference setContextUserReference(DocumentReference userReference)
{
XWikiContext context = xcontextProvider.get();
DocumentReference previousUserReference = context.getUserReference();
context.setUserReference(userReference);
return previousUserReference;
}
@Override
public void update(DocumentReference documentReference, Map<String, String> parameters)
{
try {
XWikiContext context = xcontextProvider.get();
XWiki wiki = context.getWiki();
XWikiDocument document = wiki.getDocument(documentReference, context);
boolean save = false;
String title = parameters.get("title");
if (title != null && !title.equals(document.getTitle())) {
document.setTitle(title);
save = true;
}
// Some old applications still rely on the parent/child links between documents.
// For the retro-compatibility, we synchronize the "parent" field of the document with the (real)
// hierarchical parent.
//
// But if the user has voluntary enabled the legacy "parent/child" mechanism for the breadcrumbs, we keep
// the old behaviour when location and parent/child mechanism were not linked.
//
// More information: https://jira.xwiki.org/browse/XWIKI-13493
if (!parentChildConfiguration.isParentChildMechanismEnabled()) {
DocumentReference hierarchicalParent = getHierarchicalParent(documentReference);
if (!hierarchicalParent.equals(document.getParentReference())) {
document.setParentReference(hierarchicalParent);
save = true;
}
}
if (save) {
wiki.saveDocument(document, "Update document after refactoring.", true, context);
this.logger.info("Document [{}] has been updated.", documentReference);
}
} catch (Exception e) {
this.logger.error("Failed to update the document [{}] after refactoring.", documentReference, e);
}
}
private DocumentReference getHierarchicalParent(DocumentReference documentReference)
{
final String spaceHomePage = entityReferenceProvider.getDefaultReference(EntityType.DOCUMENT).getName();
EntityReference parentOfTheSpace = documentReference.getLastSpaceReference().getParent();
boolean pageIsNotTerminal = documentReference.getName().equals(spaceHomePage);
// Case 1: The document has the location A.B.C.WebHome
// The parent should be A.B.WebHome
if (pageIsNotTerminal && parentOfTheSpace.getType() == EntityType.SPACE) {
return new DocumentReference(spaceHomePage, new SpaceReference(parentOfTheSpace));
}
// Case 2: The document has the location A.WebHome
// The parent should be Main.WebHome
if (pageIsNotTerminal && parentOfTheSpace.getType() == EntityType.WIKI) {
return new DocumentReference(spaceHomePage,
new SpaceReference(entityReferenceProvider.getDefaultReference(EntityType.SPACE).getName(),
documentReference.getWikiReference()));
}
// Case 3: The document has the location A.B
// The parent should be A.WebHome
return new DocumentReference(spaceHomePage, documentReference.getLastSpaceReference());
}
@Override
public boolean restoreDeletedDocument(long deletedDocumentId, boolean checkContextUser)
{
XWikiContext context = this.xcontextProvider.get();
XWiki xwiki = context.getWiki();
DocumentReference deletedDocumentReference = null;
try {
// Retrieve the deleted document by ID.
XWikiDeletedDocument deletedDocument = xwiki.getDeletedDocument(deletedDocumentId, context);
if (deletedDocument == null) {
logger.error("Deleted document with ID [{}] does not exist.", deletedDocumentId);
return false;
}
deletedDocumentReference = deletedDocument.getDocumentReference();
// If the document (or the translation) that we want to restore does not exist, restore it.
if (xwiki.exists(deletedDocumentReference, context)) {
// TODO: Add overwrite support maybe also with interactive (question/answer) mode.
// Default for now is to skip and log as error to restore over existing documents.
logger.error("Document [{}] with ID [{}] can not be restored. Document already exists",
deletedDocument.getFullName(), deletedDocumentId);
} else if (checkContextUser && !canRestoreDeletedDocument(deletedDocumentId, context.getUserReference())) {
logger.error("You are not allowed to restore document [{}] with ID [{}]", deletedDocumentReference,
deletedDocumentId);
} else {
// Restore the document.
xwiki.restoreFromRecycleBin(deletedDocument.getId(), "Restored from recycle bin", context);
logger.info("Document [{}] has been restored", deletedDocumentReference);
return true;
}
} catch (Exception e) {
// Try to log the document reference since it`s more useful than the ID.
if (deletedDocumentReference != null) {
logger.error("Failed to restore document [{}] with ID [{}]", deletedDocumentReference,
deletedDocumentId, e);
} else {
logger.error("Failed to restore deleted document with ID [{}]", deletedDocumentId, e);
}
}
return false;
}
@Override
public List<Long> getDeletedDocumentIds(String batchId)
{
XWikiContext context = this.xcontextProvider.get();
XWiki xwiki = context.getWiki();
List<Long> result = new ArrayList<>();
try {
XWikiDeletedDocument[] deletedDocuments =
xwiki.getRecycleBinStore().getAllDeletedDocuments(batchId, false, context, true);
for (XWikiDeletedDocument deletedDocument : deletedDocuments) {
result.add(deletedDocument.getId());
}
} catch (Exception e) {
logger.error("Failed to get deleted document IDs for batch [{}]", batchId);
}
return result;
}
@Override
public boolean canRestoreDeletedDocument(long deletedDocumentId, DocumentReference userReference)
{
boolean result = false;
XWikiContext context = this.xcontextProvider.get();
XWiki xwiki = context.getWiki();
// Remember the context user.
DocumentReference currentUserReference = context.getUserReference();
try {
XWikiDeletedDocument deletedDocument =
xwiki.getRecycleBinStore().getDeletedDocument(deletedDocumentId, context, true);
// Reuse the DeletedDocument API to check rights.
DeletedDocument deletedDocumentApi = new DeletedDocument(deletedDocument, context);
// Note: DeletedDocument API works with the current context user.
context.setUserReference(userReference);
result = deletedDocumentApi.canUndelete();
} catch (Exception e) {
logger.error("Failed to check restore rights on deleted document [{}] for user [{}]", deletedDocumentId,
userReference, e);
} finally {
// Restore the context user;
context.setUserReference(currentUserReference);
}
return result;
}
}