package com.celements.model.access;
import static com.google.common.base.Preconditions.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.annotation.Requirement;
import org.xwiki.context.Execution;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.WikiReference;
import com.celements.model.access.exception.AttachmentNotExistsException;
import com.celements.model.access.exception.ClassDocumentLoadException;
import com.celements.model.access.exception.DocumentAlreadyExistsException;
import com.celements.model.access.exception.DocumentDeleteException;
import com.celements.model.access.exception.DocumentLoadException;
import com.celements.model.access.exception.DocumentNotExistsException;
import com.celements.model.access.exception.DocumentSaveException;
import com.celements.model.access.exception.ModelAccessRuntimeException;
import com.celements.model.classes.ClassDefinition;
import com.celements.model.classes.fields.ClassField;
import com.celements.model.classes.fields.CustomClassField;
import com.celements.model.context.ModelContext;
import com.celements.model.util.ClassFieldValue;
import com.celements.model.util.ModelUtils;
import com.celements.rights.access.EAccessLevel;
import com.celements.rights.access.IRightsAccessFacadeRole;
import com.celements.rights.access.exceptions.NoAccessRightsException;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.api.Document;
import com.xpn.xwiki.doc.XWikiAttachment;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.BaseProperty;
import com.xpn.xwiki.store.XWikiStoreInterface;
@Component
public class DefaultModelAccessFacade implements IModelAccessFacade {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultModelAccessFacade.class);
public static final String DEFAULT_LANG = "";
@Requirement
protected IRightsAccessFacadeRole rightsAccess;
@Requirement
protected ModelUtils modelUtils;
@Requirement
protected ModelContext modelContext;
@Requirement
protected XWikiDocumentCreator docCreator;
@Requirement
protected Execution execution;
private XWikiContext getContext() {
return (XWikiContext) execution.getContext().getProperty(XWikiContext.EXECUTIONCONTEXT_KEY);
}
/**
* @deprecated instead use {@link #getStore()}
*/
@Deprecated
private XWiki getWiki() {
return getContext().getWiki();
}
private XWikiStoreInterface getStore() {
return getContext().getWiki().getStore();
}
@Override
public XWikiDocument getDocument(DocumentReference docRef) throws DocumentNotExistsException {
// TODO use this implementation when getDocumentReadOnly uses getDocumentFromStore
// return getDocument(docRef, DEFAULT_LANG));
return cloneDoc(getDocumentReadOnly(docRef, DEFAULT_LANG));
}
@Override
public XWikiDocument getDocument(DocumentReference docRef, String lang)
throws DocumentNotExistsException {
// TODO use this implementation when getDocumentReadOnly uses getDocumentFromStore
// return cloneDoc(getDocumentReadOnly(docRef, lang));
if (exists(docRef, lang)) {
return cloneDoc(getDocumentFromStore(docRef, lang));
} else {
throw new DocumentNotExistsException(docRef, lang);
}
}
@Override
public Document getApiDocument(XWikiDocument doc) throws NoAccessRightsException {
if (rightsAccess.hasAccessLevel(doc.getDocumentReference(), EAccessLevel.VIEW)) {
return doc.newDocument(getContext());
}
throw new NoAccessRightsException(doc.getDocumentReference(), getContext().getXWikiUser(),
EAccessLevel.VIEW);
}
/**
* returns an editable document
*/
private XWikiDocument getDocumentCloneInternal(DocumentReference docRef) {
return cloneDoc(getDocumentFromWiki(docRef));
}
/**
* returns an xwiki document for readonly usage CAUTION: never ever change anything on
* the returned XWikiDocument, because it is the object in cache. Thus the same object
* will be returned for the following requests. If you change this object, concurrent
* request might get a partially modified object, or worse, if an error occurs during
* the save (or no save call happens), the cached object will not reflect the actual
* document at all.
*/
private XWikiDocument getDocumentReadOnly(DocumentReference docRef, String lang)
throws DocumentNotExistsException {
checkNotNull(docRef);
if (exists(docRef)) {
return getDocumentFromWiki(docRef);
} else {
throw new DocumentNotExistsException(docRef);
}
}
/**
* @deprecated use {@link #getDocumentFromStore(DocumentReference, String)} instead
* <p>
* this delegation is not yet always possible because many tests do not mock this
* component and are therefore implementation dependent. changing this method to use
* the store breaks all these tests, we therefore first need a proper ModelAccess stub
* in celements.test
* </p>
*/
@Deprecated
protected XWikiDocument getDocumentFromWiki(DocumentReference docRef) {
try {
return getWiki().getDocument(docRef, getContext());
} catch (XWikiException xwe) {
throw new DocumentLoadException(docRef, xwe);
}
}
protected XWikiDocument getDocumentFromStore(DocumentReference docRef, String lang) {
String database = getContext().getDatabase();
try {
getContext().setDatabase(docRef.getWikiReference().getName());
return getStore().loadXWikiDoc(newDummyDoc(docRef, lang), getContext());
} catch (XWikiException xwe) {
throw new DocumentLoadException(docRef, xwe);
} finally {
getContext().setDatabase(database);
}
}
@Override
public XWikiDocument createDocument(DocumentReference docRef)
throws DocumentAlreadyExistsException {
checkNotNull(docRef);
if (!exists(docRef)) {
return docCreator.create(docRef);
} else {
throw new DocumentAlreadyExistsException(docRef);
}
}
@Override
public XWikiDocument getOrCreateDocument(DocumentReference docRef) {
checkNotNull(docRef);
if (exists(docRef)) {
return getDocumentCloneInternal(docRef);
} else {
return docCreator.create(docRef);
}
}
@Override
public boolean exists(DocumentReference docRef) {
boolean exists = false;
if (docRef != null) {
exists = existsFromWiki(docRef);
}
return exists;
}
@Override
public boolean exists(DocumentReference docRef, String lang) {
boolean exists = false;
if (docRef != null) {
exists = existsFromStore(docRef, lang);
}
return exists;
}
/**
* @deprecated use {@link #existsFromStore(DocumentReference, String)} instead
* <p>
* this delegation is not yet always possible because many tests do not mock this
* component and are therefore implementation dependent. changing this method to use
* the store breaks all these tests, we therefore first need a proper ModelAccess stub
* in celements.test
* </p>
*/
@Deprecated
protected boolean existsFromWiki(DocumentReference docRef) {
return getWiki().exists(docRef, getContext());
}
protected boolean existsFromStore(DocumentReference docRef, String lang) {
String database = getContext().getDatabase();
try {
getContext().setDatabase(docRef.getWikiReference().getName());
return getStore().exists(newDummyDoc(docRef, lang), getContext());
} catch (XWikiException xwe) {
throw new DocumentLoadException(docRef, xwe);
} finally {
getContext().setDatabase(database);
}
}
@Override
public void saveDocument(XWikiDocument doc) throws DocumentSaveException {
checkNotNull(doc);
saveDocument(doc, "", false);
}
@Override
public void saveDocument(XWikiDocument doc, String comment) throws DocumentSaveException {
checkNotNull(doc);
saveDocument(doc, comment, false);
}
@Override
public void saveDocument(XWikiDocument doc, String comment, boolean isMinorEdit)
throws DocumentSaveException {
checkNotNull(doc);
try {
String username = getContext().getUser();
doc.setAuthor(username);
if (doc.isNew()) {
doc.setCreator(username);
}
getWiki().saveDocument(doc, comment, isMinorEdit, getContext());
} catch (XWikiException xwe) {
throw new DocumentSaveException(doc.getDocumentReference(), xwe);
}
}
@Override
public void deleteDocument(DocumentReference docRef, boolean totrash)
throws DocumentDeleteException {
try {
deleteDocument(getDocument(docRef), totrash);
} catch (DocumentNotExistsException exc) {
LOGGER.debug("doc trying to delete does not exist '{}'", docRef, exc);
} catch (ModelAccessRuntimeException exc) {
throw new DocumentDeleteException(docRef, exc);
}
}
@Override
public void deleteDocument(XWikiDocument doc, boolean totrash) throws DocumentDeleteException {
checkNotNull(doc);
List<XWikiDocument> toDelDocs = new ArrayList<>();
try {
for (String lang : doc.getTranslationList(getContext())) {
XWikiDocument tdoc = doc.getTranslatedDocument(lang, getContext());
if ((tdoc != null) && (tdoc != doc)) {
toDelDocs.add(tdoc);
}
}
} catch (XWikiException xwe) {
throw new DocumentDeleteException(doc.getDocumentReference(), xwe);
}
toDelDocs.add(doc);
for (XWikiDocument toDel : toDelDocs) {
deleteDocumentWithoutTranslations(toDel, totrash);
}
}
@Override
public void deleteDocumentWithoutTranslations(XWikiDocument doc, boolean totrash)
throws DocumentDeleteException {
String dbBefore = getContext().getDatabase();
try {
modelContext.setWikiRef(modelUtils.extractRef(doc.getDocumentReference(),
WikiReference.class).get());
LOGGER.debug("deleteDocument: doc '{},{}', totrash '{}' dbBefore '{}' dbNow '{}'", doc,
doc.getLanguage(), totrash, dbBefore, getContext().getDatabase());
try {
getWiki().deleteDocument(doc, totrash, getContext());
} catch (XWikiException xwe) {
throw new DocumentDeleteException(doc.getDocumentReference(), xwe);
}
} finally {
getContext().setDatabase(dbBefore);
}
}
@Override
public Map<String, XWikiDocument> getTranslations(DocumentReference docRef) {
Map<String, XWikiDocument> transMap = new HashMap<>();
String database = getContext().getDatabase();
try {
getContext().setDatabase(docRef.getWikiReference().getName());
for (String lang : getStore().getTranslationList(newDummyDoc(docRef, null), getContext())) {
try {
transMap.put(lang, getDocument(docRef, lang));
} catch (DocumentNotExistsException exc) {
LOGGER.error("failed to load existing translation '{}' for doc '{}'", lang, docRef, exc);
}
}
} catch (XWikiException xwe) {
throw new DocumentLoadException(docRef, xwe);
} finally {
getContext().setDatabase(database);
}
return transMap;
}
@Override
public boolean isTranslation(XWikiDocument doc) {
return checkNotNull(doc).getTranslation() == 1;
}
/**
* We need to clone this document first, since a cached storage would return the same object for
* the following requests, so concurrent request might get a partially modified object, or worse,
* if an error occurs during the save, the cached object will not reflect the actual document at
* all.
*/
private XWikiDocument cloneDoc(XWikiDocument doc) {
if (doc.isFromCache()) {
XWikiDocument clonedDoc = doc.clone();
clonedDoc.setFromCache(false);
return clonedDoc;
}
return doc;
}
private XWikiDocument newDummyDoc(DocumentReference docRef, String lang) {
XWikiDocument doc = new XWikiDocument(docRef);
doc.setLanguage(lang);
return doc;
}
@Override
public BaseObject getXObject(DocumentReference docRef, DocumentReference classRef)
throws DocumentNotExistsException {
return Iterables.getFirst(getXObjects(getDocumentReadOnly(docRef, DEFAULT_LANG), classRef),
null);
}
@Override
public BaseObject getXObject(DocumentReference docRef, DocumentReference classRef, String key,
Object value) throws DocumentNotExistsException {
return Iterables.getFirst(getXObjects(getDocumentReadOnly(docRef, DEFAULT_LANG), classRef, key,
value), null);
}
@Override
public BaseObject getXObject(XWikiDocument doc, DocumentReference classRef) {
return Iterables.getFirst(getXObjects(doc, classRef), null);
}
@Override
public BaseObject getXObject(XWikiDocument doc, DocumentReference classRef, String key,
Object value) {
return Iterables.getFirst(getXObjects(doc, classRef, key, value), null);
}
@Override
public List<BaseObject> getXObjects(DocumentReference docRef, DocumentReference classRef)
throws DocumentNotExistsException {
return getXObjects(getDocumentReadOnly(docRef, DEFAULT_LANG), classRef);
}
@Override
public List<BaseObject> getXObjects(DocumentReference docRef, DocumentReference classRef,
String key, Object value) throws DocumentNotExistsException {
return getXObjects(getDocumentReadOnly(docRef, DEFAULT_LANG), classRef, key, value);
}
@Override
public List<BaseObject> getXObjects(DocumentReference docRef, DocumentReference classRef,
String key, Collection<?> values) throws DocumentNotExistsException {
return getXObjects(getDocumentReadOnly(docRef, DEFAULT_LANG), classRef, key, values);
}
@Override
public List<BaseObject> getXObjects(XWikiDocument doc, DocumentReference classRef) {
return getXObjects(doc, classRef, null, null);
}
@Override
public List<BaseObject> getXObjects(XWikiDocument doc, DocumentReference classRef, String key,
Object value) {
return getXObjects(doc, classRef, key, Arrays.asList(value));
}
@Override
public List<BaseObject> getXObjects(XWikiDocument doc, DocumentReference classRef, String key,
Collection<?> values) {
checkNotNull(classRef);
checkState(!isTranslation(doc));
classRef = adjustClassRef(classRef, doc);
List<BaseObject> ret = new ArrayList<>();
for (BaseObject obj : MoreObjects.firstNonNull(doc.getXObjects(classRef),
ImmutableList.<BaseObject>of())) {
if ((obj != null) && checkPropertyKeyValues(obj, key, values)) {
ret.add(obj);
}
}
return Collections.unmodifiableList(ret);
}
@Override
public Map<DocumentReference, List<BaseObject>> getXObjects(XWikiDocument doc) {
checkState(!isTranslation(doc));
Map<DocumentReference, List<BaseObject>> ret = new HashMap<>();
for (DocumentReference classRef : doc.getXObjects().keySet()) {
List<BaseObject> objs = getXObjects(doc, classRef);
if (!objs.isEmpty()) {
ret.put(classRef, objs);
}
}
return Collections.unmodifiableMap(ret);
}
private boolean checkPropertyKeyValues(BaseObject obj, String key, Collection<?> checkValues) {
boolean valid = (key == null);
if (!valid) {
checkValues = MoreObjects.firstNonNull(checkValues, Collections.emptyList());
Object val = getProperty(obj, key);
for (Object checkVal : checkValues) {
valid |= Objects.equal(val, checkVal);
}
}
return valid;
}
@Override
public com.xpn.xwiki.api.Object getApiObject(BaseObject obj) throws NoAccessRightsException {
com.xpn.xwiki.api.Object ret = null;
if (obj != null) {
try {
if (rightsAccess.hasAccessLevel(obj.getDocumentReference(), EAccessLevel.VIEW)) {
return getApiObjectWithoutRightCheck(obj);
} else {
throw new NoAccessRightsException(obj.getDocumentReference(), getContext().getXWikiUser(),
EAccessLevel.VIEW);
}
} catch (IllegalStateException exp) {
LOGGER.warn("getApiObject failed for '{}'", obj, exp);
}
}
return ret;
}
@Override
public com.xpn.xwiki.api.Object getApiObjectWithoutRightCheck(BaseObject obj) {
return obj.newObjectApi(obj, getContext());
}
@Override
public List<com.xpn.xwiki.api.Object> getApiObjects(List<BaseObject> objs) {
List<com.xpn.xwiki.api.Object> ret = new ArrayList<>();
for (BaseObject obj : objs) {
try {
if (obj != null) {
com.xpn.xwiki.api.Object apiObject = getApiObject(obj);
if (apiObject != null) {
ret.add(apiObject);
}
}
} catch (NoAccessRightsException exp) {
LOGGER.debug("getApiObjects ommits object '{}'", obj, exp);
}
}
return ret;
}
@Override
public List<com.xpn.xwiki.api.Object> getApiObjectsWithoutRightChecks(List<BaseObject> objs) {
List<com.xpn.xwiki.api.Object> ret = new ArrayList<>();
for (BaseObject obj : objs) {
if (obj != null) {
com.xpn.xwiki.api.Object apiObject = getApiObjectWithoutRightCheck(obj);
if (apiObject != null) {
ret.add(apiObject);
}
}
}
return ret;
}
@Override
public BaseObject newXObject(XWikiDocument doc, DocumentReference classRef) {
checkNotNull(doc);
checkNotNull(classRef);
classRef = adjustClassRef(classRef, doc);
try {
return doc.newXObject(classRef, getContext());
} catch (XWikiException xwe) {
throw new ClassDocumentLoadException(classRef, xwe);
}
}
@Override
public BaseObject getOrCreateXObject(XWikiDocument doc, DocumentReference classRef) {
return getOrCreateXObject(doc, classRef, null, null);
}
@Override
public BaseObject getOrCreateXObject(XWikiDocument doc, DocumentReference classRef, String key,
Object value) {
BaseObject obj = getXObject(doc, classRef, key, value);
if (obj == null) {
obj = newXObject(doc, classRef);
if (key != null) {
setProperty(obj, key, value);
}
}
return obj;
}
@Override
public boolean removeXObject(XWikiDocument doc, BaseObject objToRemove) {
return removeXObjects(doc, Arrays.asList(objToRemove));
}
@Override
public boolean removeXObjects(XWikiDocument doc, List<BaseObject> objsToRemove) {
checkNotNull(doc);
boolean changed = false;
for (BaseObject obj : new ArrayList<>(objsToRemove)) {
if (obj != null) {
changed |= doc.removeXObject(obj);
}
}
return changed;
}
@Override
public boolean removeXObjects(XWikiDocument doc, DocumentReference classRef) {
return removeXObjects(doc, classRef, null, null);
}
@Override
public boolean removeXObjects(XWikiDocument doc, DocumentReference classRef, String key,
Object value) {
return removeXObjects(doc, classRef, key, Arrays.asList(value));
}
@Override
public boolean removeXObjects(XWikiDocument doc, DocumentReference classRef, String key,
Collection<?> values) {
return removeXObjects(doc, getXObjects(doc, classRef, key, values));
}
@Override
public Object getProperty(DocumentReference docRef, DocumentReference classRef, String name)
throws DocumentNotExistsException {
return getProperty(getDocumentReadOnly(docRef, DEFAULT_LANG), classRef, name);
}
@Override
public Object getProperty(XWikiDocument doc, DocumentReference classRef, String name) {
return getProperty(getXObject(doc, classRef), name);
}
@Override
public Object getProperty(BaseObject obj, String name) {
Object value = null;
BaseProperty prop = getBaseProperty(obj, name);
if (prop != null) {
value = prop.getValue();
if (value instanceof String) {
// avoid comparing empty string to null
value = Strings.emptyToNull(value.toString().trim());
} else if (value instanceof Date) {
// avoid returning Timestamp since Timestamp.equals(Date) always returns false
value = new Date(((Date) value).getTime());
}
}
return value;
}
private BaseProperty getBaseProperty(BaseObject obj, String key) {
BaseProperty prop = null;
try {
if (obj != null) {
prop = (BaseProperty) obj.get(key);
}
} catch (XWikiException | ClassCastException exc) {
// does not happen since
// XWikiException is never thrown in BaseObject.get()
// BaseObject only contains BaseProperties
LOGGER.error("should not happen", exc);
}
return prop;
}
@Override
public <T> T getProperty(DocumentReference docRef, ClassField<T> field)
throws DocumentNotExistsException {
return resolvePropertyValue(field, getProperty(docRef, field.getClassRef(), field.getName()));
}
@Override
public <T> T getProperty(XWikiDocument doc, ClassField<T> field) {
return resolvePropertyValue(field, getProperty(doc, field.getClassRef(), field.getName()));
}
private <T> T resolvePropertyValue(ClassField<T> field, Object value) {
try {
if (field instanceof CustomClassField) {
return ((CustomClassField<T>) field).resolve(value);
} else {
return field.getType().cast(value);
}
} catch (ClassCastException | IllegalArgumentException ex) {
throw new IllegalArgumentException("Field '" + field + "' ill defined, expecting type '"
+ field.getType() + "' but got '" + value.getClass() + "'", ex);
}
}
@Override
public List<ClassFieldValue<?>> getProperties(XWikiDocument doc, ClassDefinition classDef) {
List<ClassFieldValue<?>> ret = new ArrayList<>();
for (ClassField<?> field : classDef.getFields()) {
ret.add(new ClassFieldValue<>(castField(field), getProperty(doc, field)));
}
return ret;
}
// unchecked suppression is ok because every wildcard extends Object
@SuppressWarnings("unchecked")
private ClassField<Object> castField(ClassField<?> field) {
return (ClassField<Object>) field;
}
@Override
public boolean setProperty(BaseObject obj, String name, Object value) {
boolean hasChange = !Objects.equal(value, getProperty(obj, name));
if (hasChange) {
if (value instanceof Collection) {
value = Joiner.on('|').join((Collection<?>) value);
}
obj.set(name, value, getContext());
}
return hasChange;
}
@Override
public <T> XWikiDocument setProperty(DocumentReference docRef, ClassField<T> field, T value)
throws DocumentNotExistsException {
XWikiDocument doc = getDocument(docRef);
setProperty(doc, field, value);
return doc;
}
@Override
public <T> boolean setProperty(XWikiDocument doc, ClassField<T> field, T value) {
try {
return setProperty(getOrCreateXObject(doc, field.getClassRef()), field.getName(),
serializePropertyValue(field, value));
} catch (ClassCastException ex) {
throw new IllegalArgumentException("CelObjectField ill defined: " + field, ex);
}
}
@Override
public <T> boolean setProperty(XWikiDocument doc, ClassFieldValue<T> fieldValue) {
return setProperty(doc, fieldValue.getField(), fieldValue.getValue());
}
private <T> Object serializePropertyValue(ClassField<T> field, T value) {
try {
if (field instanceof CustomClassField) {
return ((CustomClassField<T>) field).serialize(value);
} else {
return value;
}
} catch (ClassCastException | IllegalArgumentException ex) {
throw new IllegalArgumentException("Field '" + field + "' ill defined", ex);
}
}
@Override
public XWikiAttachment getAttachmentNameEqual(XWikiDocument document, String filename)
throws AttachmentNotExistsException {
for (XWikiAttachment attach : document.getAttachmentList()) {
if ((attach != null) && attach.getFilename().equals(filename)) {
return attach;
}
}
LOGGER.debug("getAttachmentNameEqual: not found! file: [{}], doc: [{}], docref: [{}]", filename,
document, document.getDocumentReference());
// FIXME empty or null filename leads to exception:
// java.lang.IllegalArgumentException: An Entity Reference name cannot be null or
// empty
if (Strings.isNullOrEmpty(filename)) {
throw new AttachmentNotExistsException(null);
} else {
throw new AttachmentNotExistsException(new AttachmentReference(filename,
document.getDocumentReference()));
}
}
/**
* forces same wikiRef to classRef as for onDoc
*/
private DocumentReference adjustClassRef(DocumentReference classRef, XWikiDocument onDoc) {
return modelUtils.adjustRef(classRef, DocumentReference.class, modelUtils.extractRef(
onDoc.getDocumentReference(), WikiReference.class).or(modelContext.getWikiRef()));
}
}