Skip to content

XWIKI-22665: Required rights should be cached #3695

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions xwiki-platform-core/xwiki-platform-oldcore/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@
<artifactId>batik-svgrasterizer</artifactId>
</dependency>

<!-- Guava, currently only used in SimpleDocumentCache -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>

<!-- Misc -->
<!-- Better date manipulation than what JDK provides -->
<dependency>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* 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.internal.document;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;

import javax.inject.Inject;

import org.apache.commons.lang3.function.FailableFunction;
import org.xwiki.bridge.event.DocumentCreatedEvent;
import org.xwiki.bridge.event.DocumentDeletedEvent;
import org.xwiki.bridge.event.DocumentUpdatedEvent;
import org.xwiki.cache.Cache;
import org.xwiki.cache.CacheException;
import org.xwiki.cache.CacheManager;
import org.xwiki.cache.config.CacheConfiguration;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.annotation.InstantiationStrategy;
import org.xwiki.component.descriptor.ComponentInstantiationStrategy;
import org.xwiki.component.phase.Disposable;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.observation.AbstractEventListener;
import org.xwiki.observation.EventListener;
import org.xwiki.observation.ObservationManager;
import org.xwiki.observation.event.Event;

import com.google.common.util.concurrent.Striped;
import com.xpn.xwiki.doc.XWikiDocument;

/**
* A cache that allows caching a single value per document reference.
*
* @param <C> the type of the data stored in the cache
* @param <E> the type of the exception that can be thrown by the provider
* @version $Id$
*/
@Component
@InstantiationStrategy(ComponentInstantiationStrategy.PER_LOOKUP)
public class DefaultSimpleDocumentCache<C, E extends Throwable> implements Disposable, SimpleDocumentCache<C, E>
{
@Inject
private ObservationManager observationManager;

@Inject
private CacheManager cacheManager;

@Inject
private EntityReferenceSerializer<String> serializer;

private Cache<C> cache;

private Listener listener;

// Use a read-write lock to ensure that during cache invalidation, no values are computed based on outdated data.
// Use a striped lock to ensure that the removal of one value doesn't block the setting of other values.
private final Striped<ReadWriteLock> locks = Striped.readWriteLock(16);

private class Listener extends AbstractEventListener
{
Listener(String name)
{
super(name, new DocumentCreatedEvent(), new DocumentUpdatedEvent(), new DocumentDeletedEvent());
}

@Override
public void onEvent(Event event, Object source, Object data)
{
XWikiDocument doc = (XWikiDocument) source;
remove(doc.getDocumentReference());
}
}

@Override
public synchronized void initializeCache(CacheConfiguration cacheConfiguration) throws CacheException
{
// If the cache has already been created, dispose the existing one and create a new one.
if (this.cache != null) {
dispose();
}

this.cache = this.cacheManager.createNewCache(cacheConfiguration);
this.listener = new Listener(cacheConfiguration.getConfigurationId());
this.observationManager.addListener(this.listener, EventListener.CACHE_INVALIDATION_DEFAULT_PRIORITY);
}

@Override
public C get(DocumentReference documentReference, FailableFunction<DocumentReference, C, E> provider) throws E
{
String key = getKey(documentReference);
C result = this.cache.get(key);

if (result == null) {
// Use a read lock to ensure that we don't compute a new value based on an old document while an
// invalidation is running. We don't care about computing several times in parallel.
Lock lock = this.locks.get(key).readLock();
lock.lock();
try {
result = provider.apply(documentReference);
this.cache.set(key, result);
} finally {
lock.unlock();
}
}

return result;
}

/**
* Remove the value associated with the provided document reference.
*
* @param documentReference the reference of the document
*/
public void remove(DocumentReference documentReference)
{
String key = getKey(documentReference);
// Use a write lock to ensure that we don't compute a new value based on an old document.
Lock lock = this.locks.get(key).writeLock();
lock.lock();
try {
this.cache.remove(key);
} finally {
lock.unlock();
}
}

private String getKey(DocumentReference documentReference)
{
return this.serializer.serialize(documentReference);
}

@Override
public void dispose()
{
if (this.cache != null) {
this.cache.dispose();
}

this.observationManager.removeListener(this.listener.getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/
package org.xwiki.internal.document;

import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -60,6 +61,20 @@ public class DocumentRequiredRightsReader
*/
public static final String PROPERTY_NAME = "level";

private static final DocumentRequiredRights ENFORCED_EMPTY = new DocumentRequiredRights(true, Set.of());

private static final DocumentRequiredRights ENFORCED_SCRIPT = new DocumentRequiredRights(true,
Set.of(new DocumentRequiredRight(Right.SCRIPT, EntityType.DOCUMENT)));

private static final DocumentRequiredRights ENFORCED_PROGRAMMING = new DocumentRequiredRights(true,
Set.of(new DocumentRequiredRight(Right.PROGRAM, null)));

private static final DocumentRequiredRights ENFORCED_ADMIN = new DocumentRequiredRights(true,
Set.of(new DocumentRequiredRight(Right.ADMIN, EntityType.WIKI)));

private static final List<DocumentRequiredRights> STATIC_INSTANCES = List.of(ENFORCED_EMPTY, ENFORCED_SCRIPT,
ENFORCED_PROGRAMMING, ENFORCED_ADMIN);

@Inject
private Logger logger;

Expand All @@ -71,15 +86,33 @@ public class DocumentRequiredRightsReader
*/
public DocumentRequiredRights readRequiredRights(XWikiDocument document)
{
return new DocumentRequiredRights(document.isEnforceRequiredRights(),
document.getXObjects(CLASS_REFERENCE).stream()
.filter(Objects::nonNull)
.map(this::readRequiredRight)
// Don't allow edit right/edit right implies no extra right.
// Filter out invalid values.
.filter(requiredRight ->
!Right.EDIT.equals(requiredRight.right()) && !Right.ILLEGAL.equals(requiredRight.right()))
.collect(Collectors.toUnmodifiableSet()));
boolean enforce = document.isEnforceRequiredRights();
Set<DocumentRequiredRight> rights = document.getXObjects(CLASS_REFERENCE).stream()
.filter(Objects::nonNull)
.map(this::readRequiredRight)
// Don't allow edit right/edit right implies no extra right.
// Filter out invalid values.
.filter(requiredRight ->
!Right.EDIT.equals(requiredRight.right()) && !Right.ILLEGAL.equals(requiredRight.right()))
.collect(Collectors.toUnmodifiableSet());

// Try returning a static instance to avoid creating lots of objects that contain the same values as most
// documents will be in the case of one of the static instances. This is also to reduce the memory usage of
// the cache.
if (!enforce && rights.isEmpty()) {
return DocumentRequiredRights.EMPTY;
}

// Return the static instance that has the same set of rights.
if (enforce) {
for (DocumentRequiredRights staticInstance : STATIC_INSTANCES) {
if (staticInstance.rights().equals(rights)) {
return staticInstance;
}
}
}

return new DocumentRequiredRights(enforce, rights);
}

private DocumentRequiredRight readRequiredRight(BaseObject object)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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.internal.document;

import org.apache.commons.lang3.function.FailableFunction;
import org.xwiki.cache.CacheException;
import org.xwiki.cache.config.CacheConfiguration;
import org.xwiki.component.annotation.ComponentRole;
import org.xwiki.model.reference.DocumentReference;

/**
* A simple cache for documents.
*
* @param <C> the type of the cached value
* @param <E> the type of the exception that can be thrown by the provider
* @version $Id$
*/
// We need to use @ComponentRole here as @Role doesn't support generic component implementations, and there is no
// non-deprecated way to achieve this.
@SuppressWarnings({ "java:S1874", "deprecation" })
@ComponentRole
public interface SimpleDocumentCache<C, E extends Throwable>
{
/**
* Initialize the cache.
*
* @param cacheConfiguration the cache configuration
* @throws CacheException if the cache couldn’t be created
*/
void initializeCache(CacheConfiguration cacheConfiguration) throws CacheException;

/**
* Get the value associated with the provided document reference, or compute it when missing. The computed valued is
* stored in the cache.
*
* @param documentReference the reference of the document
* @param provider the provider to compute the value if it isn't in the cache. The document used to compute the
* cached value needs to be loaded inside the provider to ensure that no stale data is cached
* @return the value
*/
C get(DocumentReference documentReference, FailableFunction<DocumentReference, C, E> provider) throws E;
}
Original file line number Diff line number Diff line change
Expand Up @@ -282,5 +282,5 @@ org.xwiki.evaluation.internal.DefaultObjectEvaluator
org.xwiki.evaluation.internal.VelocityObjectPropertyEvaluator
org.xwiki.internal.document.DocumentOverrideListener
org.xwiki.internal.document.DocumentRequiredRightsReader
org.xwiki.internal.document.DefaultDocumentRequiredRightsManager
org.xwiki.internal.document.RequiredRightClassMandatoryDocumentInitializer
org.xwiki.internal.document.DefaultSimpleDocumentCache
Loading
Loading