mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-16 10:16:16 +01:00
One index per type and parallel indexing (#1781)
Before this change the search uses a single index which distinguishes types (repositories, users, etc.) with a field (_type).
But it has turned out that this could lead to problems, in particular if different types have the same field and uses different analyzers for those fields. The following links show even more problems of a combined index:
https://www.elastic.co/blog/index-vs-type
https://www.elastic.co/guide/en/elasticsearch/reference/6.0/removal-of-types.html
With this change every type becomes its own index and the SearchEngine gets an api to modify multiple indices at once to remove all documents from all indices, which are related to a specific repository, for example.
The search uses another new api to coordinate the indexing, the central work queue.
The central work queue is able to coordinate long-running or resource intensive tasks. It is able to run tasks in parallel, but can also run tasks which targets the same resources in sequence. The queue is also persistent and can restore queued tasks after restart.
Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
8
gradle/changelog/one_index_per_type.yaml
Normal file
8
gradle/changelog/one_index_per_type.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
- type: Added
|
||||||
|
description: Central Work Queue for coordinating long-running tasks ([#1781](https://github.com/scm-manager/scm-manager/pull/1781))
|
||||||
|
- type: Changed
|
||||||
|
description: One index per type instead of one index for all types ([#1781](https://github.com/scm-manager/scm-manager/pull/1781))
|
||||||
|
- type: Added
|
||||||
|
description: Api to modify mutliple indices at once ([#1781](https://github.com/scm-manager/scm-manager/pull/1781))
|
||||||
|
- type: Changed
|
||||||
|
description: Use central work queue for all indexing tasks ([#1781](https://github.com/scm-manager/scm-manager/pull/1781))
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
import sonia.scm.HandlerEventType;
|
import sonia.scm.HandlerEventType;
|
||||||
import sonia.scm.event.HandlerEvent;
|
import sonia.scm.event.HandlerEvent;
|
||||||
|
|
||||||
@@ -33,11 +34,14 @@ import sonia.scm.event.HandlerEvent;
|
|||||||
* @param <T> type of indexed item
|
* @param <T> type of indexed item
|
||||||
* @since 2.22.0
|
* @since 2.22.0
|
||||||
*/
|
*/
|
||||||
|
@Beta
|
||||||
public final class HandlerEventIndexSyncer<T> {
|
public final class HandlerEventIndexSyncer<T> {
|
||||||
|
|
||||||
|
private final SearchEngine searchEngine;
|
||||||
private final Indexer<T> indexer;
|
private final Indexer<T> indexer;
|
||||||
|
|
||||||
public HandlerEventIndexSyncer(Indexer<T> indexer) {
|
public HandlerEventIndexSyncer(SearchEngine searchEngine, Indexer<T> indexer) {
|
||||||
|
this.searchEngine = searchEngine;
|
||||||
this.indexer = indexer;
|
this.indexer = indexer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,17 +53,16 @@ public final class HandlerEventIndexSyncer<T> {
|
|||||||
public void handleEvent(HandlerEvent<T> event) {
|
public void handleEvent(HandlerEvent<T> event) {
|
||||||
HandlerEventType type = event.getEventType();
|
HandlerEventType type = event.getEventType();
|
||||||
if (type.isPost()) {
|
if (type.isPost()) {
|
||||||
updateIndex(type, event.getItem());
|
SerializableIndexTask<T> task = createTask(type, event.getItem());
|
||||||
|
searchEngine.forType(indexer.getType()).update(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateIndex(HandlerEventType type, T item) {
|
private SerializableIndexTask<T> createTask(HandlerEventType type, T item) {
|
||||||
try (Indexer.Updater<T> updater = indexer.open()) {
|
if (type == HandlerEventType.DELETE) {
|
||||||
if (type == HandlerEventType.DELETE) {
|
return indexer.createDeleteTask(item);
|
||||||
updater.delete(item);
|
} else {
|
||||||
} else {
|
return indexer.createStoreTask(item);
|
||||||
updater.store(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
import com.google.common.annotations.Beta;
|
import com.google.common.annotations.Beta;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can be used to index objects for full text searches.
|
* Can be used to index objects for full text searches.
|
||||||
@@ -32,7 +33,15 @@ import com.google.common.annotations.Beta;
|
|||||||
* @since 2.21.0
|
* @since 2.21.0
|
||||||
*/
|
*/
|
||||||
@Beta
|
@Beta
|
||||||
public interface Index<T> extends AutoCloseable {
|
public interface Index<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns details such as name and type of index.
|
||||||
|
*
|
||||||
|
* @return details of index
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
IndexDetails getDetails();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the given object in the index.
|
* Store the given object in the index.
|
||||||
@@ -53,12 +62,6 @@ public interface Index<T> extends AutoCloseable {
|
|||||||
*/
|
*/
|
||||||
Deleter delete();
|
Deleter delete();
|
||||||
|
|
||||||
/**
|
|
||||||
* Close index and commit changes.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
void close();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deleter provides an api to delete object from index.
|
* Deleter provides an api to delete object from index.
|
||||||
*
|
*
|
||||||
@@ -66,27 +69,6 @@ public interface Index<T> extends AutoCloseable {
|
|||||||
*/
|
*/
|
||||||
interface Deleter {
|
interface Deleter {
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an api which allows deletion of objects from the type of this index.
|
|
||||||
* @return type restricted delete api
|
|
||||||
*/
|
|
||||||
ByTypeDeleter byType();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an api which allows deletion of objects of every type.
|
|
||||||
* @return unrestricted delete api for all types.
|
|
||||||
*/
|
|
||||||
AllTypesDeleter allTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete api for the type of the index. This means, that only entries for this
|
|
||||||
* type will be deleted.
|
|
||||||
*
|
|
||||||
* @since 2.23.0
|
|
||||||
*/
|
|
||||||
interface ByTypeDeleter {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the object with the given id and type from index.
|
* Delete the object with the given id and type from index.
|
||||||
* @param id id of object
|
* @param id id of object
|
||||||
@@ -104,27 +86,15 @@ public interface Index<T> extends AutoCloseable {
|
|||||||
* @param repositoryId id of repository
|
* @param repositoryId id of repository
|
||||||
*/
|
*/
|
||||||
void byRepository(String repositoryId);
|
void byRepository(String repositoryId);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete api for the overall index regarding all types.
|
|
||||||
*
|
|
||||||
* @since 2.23.0
|
|
||||||
*/
|
|
||||||
interface AllTypesDeleter {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all objects which are related to the given repository from index regardless of their type.
|
* Delete all objects which are related the given type and repository from index.
|
||||||
* @param repositoryId repository id
|
*
|
||||||
|
* @param repository repository
|
||||||
*/
|
*/
|
||||||
void byRepository(String repositoryId);
|
default void byRepository(Repository repository) {
|
||||||
|
byRepository(repository.getId());
|
||||||
/**
|
}
|
||||||
* Delete all objects with the given type from index.
|
|
||||||
* This method is mostly useful if the index type has changed and the old type (in form of a class)
|
|
||||||
* is no longer available.
|
|
||||||
* @param typeName type name of objects
|
|
||||||
*/
|
|
||||||
void byTypeName(String typeName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
47
scm-core/src/main/java/sonia/scm/search/IndexDetails.java
Normal file
47
scm-core/src/main/java/sonia/scm/search/IndexDetails.java
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details of an index.
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
public interface IndexDetails {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns type of objects which are indexed.
|
||||||
|
* @return type of objects
|
||||||
|
*/
|
||||||
|
Class<?> getType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the index (e.g. `default`)
|
||||||
|
* @return name
|
||||||
|
*/
|
||||||
|
String getName();
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ package sonia.scm.search;
|
|||||||
import com.google.common.annotations.Beta;
|
import com.google.common.annotations.Beta;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,7 +37,7 @@ import java.util.Locale;
|
|||||||
*/
|
*/
|
||||||
@Beta
|
@Beta
|
||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
public class IndexOptions {
|
public class IndexOptions implements Serializable {
|
||||||
|
|
||||||
private final Type type;
|
private final Type type;
|
||||||
private final Locale locale;
|
private final Locale locale;
|
||||||
|
|||||||
51
scm-core/src/main/java/sonia/scm/search/IndexTask.java
Normal file
51
scm-core/src/main/java/sonia/scm/search/IndexTask.java
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A task which updates an index.
|
||||||
|
* @param <T> type of indexed objects
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface IndexTask<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute operations on the index.
|
||||||
|
* @param index index to update
|
||||||
|
*/
|
||||||
|
void update(Index<T> index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called after work is committed to the index.
|
||||||
|
*/
|
||||||
|
default void afterUpdate() {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
import sonia.scm.plugin.ExtensionPoint;
|
import sonia.scm.plugin.ExtensionPoint;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +36,7 @@ import sonia.scm.plugin.ExtensionPoint;
|
|||||||
* @since 2.22.0
|
* @since 2.22.0
|
||||||
* @see HandlerEventIndexSyncer
|
* @see HandlerEventIndexSyncer
|
||||||
*/
|
*/
|
||||||
|
@Beta
|
||||||
@ExtensionPoint
|
@ExtensionPoint
|
||||||
public interface Indexer<T> {
|
public interface Indexer<T> {
|
||||||
|
|
||||||
@@ -54,42 +56,48 @@ public interface Indexer<T> {
|
|||||||
int getVersion();
|
int getVersion();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the index and return an updater for the given type.
|
* Returns task which re index all items.
|
||||||
*
|
* @return task to re index all
|
||||||
* @return updater with open index
|
* @since 2.23.0
|
||||||
*/
|
*/
|
||||||
Updater<T> open();
|
Class<? extends ReIndexAllTask<T>> getReIndexAllTask();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updater for index.
|
* Creates a task which stores the given item in the index.
|
||||||
*
|
* @param item item to store
|
||||||
* @param <T> type to index
|
* @return task which stores the item
|
||||||
|
* @since 2.23.0
|
||||||
*/
|
*/
|
||||||
interface Updater<T> extends AutoCloseable {
|
SerializableIndexTask<T> createStoreTask(T item);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the given item in the index.
|
* Creates a task which deletes the given item from index.
|
||||||
*
|
* @param item item to delete
|
||||||
* @param item item to index
|
* @return task which deletes the item
|
||||||
*/
|
* @since 2.23.0
|
||||||
void store(T item);
|
*/
|
||||||
|
SerializableIndexTask<T> createDeleteTask(T item);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the given item from the index
|
* Abstract class which builds the foundation for tasks which re-index all items.
|
||||||
*
|
*
|
||||||
* @param item item to delete
|
* @since 2.23.0
|
||||||
*/
|
*/
|
||||||
void delete(T item);
|
abstract class ReIndexAllTask<T> implements IndexTask<T> {
|
||||||
|
|
||||||
/**
|
private final IndexLogStore logStore;
|
||||||
* Re index all existing items.
|
private final Class<T> type;
|
||||||
*/
|
private final int version;
|
||||||
void reIndexAll();
|
|
||||||
|
|
||||||
/**
|
protected ReIndexAllTask(IndexLogStore logStore, Class<T> type, int version) {
|
||||||
* Close the given index.
|
this.logStore = logStore;
|
||||||
*/
|
this.type = type;
|
||||||
void close();
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterUpdate() {
|
||||||
|
logStore.defaultIndex().log(type, version);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,10 @@
|
|||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
import com.google.common.annotations.Beta;
|
import com.google.common.annotations.Beta;
|
||||||
|
import sonia.scm.ModelObject;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link SearchEngine} is the main entry point for indexing and searching.
|
* The {@link SearchEngine} is the main entry point for indexing and searching.
|
||||||
@@ -61,6 +63,80 @@ public interface SearchEngine {
|
|||||||
*/
|
*/
|
||||||
ForType<Object> forType(String name);
|
ForType<Object> forType(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an api which allows the modification of multiple indices at once.
|
||||||
|
* @return api to modify multiple indices
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
ForIndices forIndices();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Api for modifying multiple indices at once.
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
interface ForIndices {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method can be used to filter the indices.
|
||||||
|
* If no predicate is used the tasks are enqueued for every existing index.
|
||||||
|
*
|
||||||
|
* @param predicate predicate to filter indices
|
||||||
|
* @return {@code this}
|
||||||
|
*/
|
||||||
|
ForIndices matching(Predicate<IndexDetails> predicate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a lock for a specific resource. By default, a lock for the whole index is used.
|
||||||
|
* If one or more specific resources are locked, than the lock is applied only for those resources
|
||||||
|
* and tasks which targets other resources of the same index can run in parallel.
|
||||||
|
*
|
||||||
|
* @param resource specific resource to lock
|
||||||
|
* @return {@code this}
|
||||||
|
*/
|
||||||
|
ForIndices forResource(String resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is a shortcut for {@link #forResource(String)} with the id of the given resource.
|
||||||
|
*
|
||||||
|
* @param resource resource in form of model object
|
||||||
|
* @return {@code this}
|
||||||
|
*/
|
||||||
|
default ForIndices forResource(ModelObject resource) {
|
||||||
|
return forResource(resource.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify options for the index.
|
||||||
|
* If not used the default options will be used.
|
||||||
|
* @param options index options
|
||||||
|
* @return {@code this}
|
||||||
|
* @see IndexOptions#defaults()
|
||||||
|
*/
|
||||||
|
ForIndices withOptions(IndexOptions options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the task and execute it for every index
|
||||||
|
* which are matching the predicate ({@link #matching(Predicate)}.
|
||||||
|
* The task is executed asynchronous and will be finished some time in the future.
|
||||||
|
* <strong>Note:</strong> the task must be serializable because it is submitted to the
|
||||||
|
* {@link sonia.scm.work.CentralWorkQueue}.
|
||||||
|
* For more information on task serialization have a look at the
|
||||||
|
* {@link sonia.scm.work.CentralWorkQueue} documentation.
|
||||||
|
*
|
||||||
|
* @param task serializable task for updating multiple indices
|
||||||
|
*/
|
||||||
|
void batch(SerializableIndexTask<?> task);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the task and executes it for every index
|
||||||
|
* which are matching the predicate ({@link #matching(Predicate)}.
|
||||||
|
* The task is executed asynchronously and will finish at some unknown point in the future.
|
||||||
|
*
|
||||||
|
* @param task task for updating multiple indices
|
||||||
|
*/
|
||||||
|
void batch(Class<? extends IndexTask<?>> task);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search and index api.
|
* Search and index api.
|
||||||
*
|
*
|
||||||
@@ -87,10 +163,44 @@ public interface SearchEngine {
|
|||||||
ForType<T> withIndex(String name);
|
ForType<T> withIndex(String name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an index object which provides method to update the search index.
|
* Apply a lock for a specific resource. By default, a lock for the whole index is used.
|
||||||
* @return index object
|
* If one or more specific resources are locked, then the lock is applied only for those resources
|
||||||
|
* and tasks which target other resources of the same index can run in parallel.
|
||||||
|
*
|
||||||
|
* @param resource specific resource to lock
|
||||||
|
* @return {@code this}
|
||||||
*/
|
*/
|
||||||
Index<T> getOrCreate();
|
ForType<T> forResource(String resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is a shortcut for {@link #forResource(String)} with the id of the given resource.
|
||||||
|
*
|
||||||
|
* @param resource resource in form of model object
|
||||||
|
* @return {@code this}
|
||||||
|
*/
|
||||||
|
default ForType<T> forResource(ModelObject resource) {
|
||||||
|
return forResource(resource.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits a task to update the index.
|
||||||
|
* The task is executed asynchronously and will finish at some unknown point in the future.
|
||||||
|
* <strong>Note:</strong> the task must be serializable because it is submitted to the
|
||||||
|
* {@link sonia.scm.work.CentralWorkQueue},
|
||||||
|
* for more information about the task serialization have a look at the
|
||||||
|
* {@link sonia.scm.work.CentralWorkQueue} documentation.
|
||||||
|
*
|
||||||
|
* @param task serializable task for updating the index
|
||||||
|
*/
|
||||||
|
void update(SerializableIndexTask<T> task);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits a task to update the index.
|
||||||
|
* The task is executed asynchronous and will be finished some time in the future.
|
||||||
|
*
|
||||||
|
* @param task task for updating multiple indices
|
||||||
|
*/
|
||||||
|
void update(Class<? extends IndexTask<T>> task);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a query builder object which can be used to search the index.
|
* Returns a query builder object which can be used to search the index.
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A serializable version of {@link IndexTask}.
|
||||||
|
*
|
||||||
|
* @param <T> type of indexed objects
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface SerializableIndexTask<T> extends IndexTask<T>, Serializable {
|
||||||
|
}
|
||||||
174
scm-core/src/main/java/sonia/scm/work/CentralWorkQueue.java
Normal file
174
scm-core/src/main/java/sonia/scm/work/CentralWorkQueue.java
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
import sonia.scm.ModelObject;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link CentralWorkQueue} provides an api to submit and coordinate long-running or resource intensive tasks.
|
||||||
|
*
|
||||||
|
* The tasks are executed in parallel, but if some tasks access the same resource this can become a problem.
|
||||||
|
* To avoid this problem a task can be enqueued with a lock e.g.:
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* queue.append().locks("my-resources").enqueue(MyTask.class)
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* No tasks with the same lock will run in parallel. The example above locks a whole group of resources.
|
||||||
|
* It is possible to assign the lock to a more specific resource by adding the id parameter to the lock e.g.:
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* queue.append().locks("my-resources", "42").enqueue(MyTask.class)
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* This will ensure, that no task with a lock for the <i>my-resource 42</i> or for <i>my-resources</i> will run at the
|
||||||
|
* same time as the task which is enqueued in the example above.
|
||||||
|
* But this will also allow a task for <i>my-resources</i> with an id other than <i>42</i> can run in parallel.
|
||||||
|
*
|
||||||
|
* All tasks are executed with the permissions of the user which enqueues the task.
|
||||||
|
* If the task should run as admin, the {@link Enqueue#runAsAdmin()} method can be used.
|
||||||
|
*
|
||||||
|
* Tasks which could not be finished,
|
||||||
|
* before a restart of shutdown of the server, will be restored and executed on startup.
|
||||||
|
* In order to achieve the persistence of tasks,
|
||||||
|
* the enqueued task must be provided as a class or it must be serializable.
|
||||||
|
* This could become unhandy if the task has parameters and dependencies which must be injected.
|
||||||
|
* The injected objects should not be serialized with the task.
|
||||||
|
* In order to avoid this, dependencies should be declared as {@code transient}
|
||||||
|
* and injected via setters instead of the constructor parameters e.g.:
|
||||||
|
*
|
||||||
|
* <pre><code>
|
||||||
|
* public class MyTask implements Task {
|
||||||
|
*
|
||||||
|
* private final Repository repository;
|
||||||
|
* private transient RepositoryServiceFactory repositoryServiceFactory;
|
||||||
|
*
|
||||||
|
* public MyTask(Repository repository) {
|
||||||
|
* this.repository = repository;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* {@code @}Inject
|
||||||
|
* public void setRepositoryServiceFactory(RepositoryServiceFactory repositoryServiceFactory) {
|
||||||
|
* this.repositoryServiceFactory = repositoryServiceFactory;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* {@code @}Override
|
||||||
|
* public void run() {
|
||||||
|
* // do something with the repository and the repositoryServiceFactory
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* }
|
||||||
|
* </code></pre>
|
||||||
|
*
|
||||||
|
* The {@link CentralWorkQueue} will inject the requested members before the {@code run} method of the task is executed.
|
||||||
|
*
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
public interface CentralWorkQueue {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a new task to the central work queue.
|
||||||
|
* The method will return a builder interface to configure how the task will be enqueued.
|
||||||
|
*
|
||||||
|
* @return builder api for enqueue a task
|
||||||
|
*/
|
||||||
|
Enqueue append();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the count of pending or running tasks.
|
||||||
|
*
|
||||||
|
* @return count of pending or running tasks
|
||||||
|
*/
|
||||||
|
int getSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder interface for the enqueueing of a new task.
|
||||||
|
*/
|
||||||
|
interface Enqueue {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a lock for the given resource type.
|
||||||
|
* For more information on locks see the class documentation ({@link CentralWorkQueue}).
|
||||||
|
*
|
||||||
|
* @param resourceType resource type to lock
|
||||||
|
* @return {@code this}
|
||||||
|
* @see CentralWorkQueue
|
||||||
|
*/
|
||||||
|
Enqueue locks(String resourceType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a lock for the resource with the given type and id.
|
||||||
|
* Note if the id is {@code null} the whole resource type is locked.
|
||||||
|
* For more information on locks see the class documentation ({@link CentralWorkQueue}).
|
||||||
|
*
|
||||||
|
* @param resourceType resource type to lock
|
||||||
|
* @param id id of resource to lock
|
||||||
|
* @return {@code this}
|
||||||
|
* @see CentralWorkQueue
|
||||||
|
*/
|
||||||
|
Enqueue locks(String resourceType, @Nullable String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a lock for the resource with the given type and the id from the given {@link ModelObject}.
|
||||||
|
* For more information on locks see the class documentation ({@link CentralWorkQueue}).
|
||||||
|
*
|
||||||
|
* @param resourceType resource type to lock
|
||||||
|
* @param object which holds the id of the resource to lock
|
||||||
|
* @return {@code this}
|
||||||
|
* @see CentralWorkQueue
|
||||||
|
*/
|
||||||
|
default Enqueue locks(String resourceType, ModelObject object) {
|
||||||
|
return locks(resourceType, object.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the enqueued task with administrator permission.
|
||||||
|
*
|
||||||
|
* @return {@code this}
|
||||||
|
*/
|
||||||
|
Enqueue runAsAdmin();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue the given task to {@link CentralWorkQueue}.
|
||||||
|
* <strong>Warning: </strong> Ensure that the task is serializable.
|
||||||
|
* If the task is not serializable an {@link NonPersistableTaskException} will be thrown.
|
||||||
|
* For more information about the persistence of tasks see class documentation.
|
||||||
|
*
|
||||||
|
* @param task serializable task to enqueue
|
||||||
|
* @see CentralWorkQueue
|
||||||
|
*/
|
||||||
|
void enqueue(Task task);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue the given task to {@link CentralWorkQueue}.
|
||||||
|
* @param task task to enqueue
|
||||||
|
*/
|
||||||
|
void enqueue(Class<? extends Runnable> task);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when a task is enqueued to the {@link CentralWorkQueue} which cannot persisted.
|
||||||
|
*
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
public final class NonPersistableTaskException extends RuntimeException {
|
||||||
|
public NonPersistableTaskException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
scm-core/src/main/java/sonia/scm/work/Task.java
Normal file
38
scm-core/src/main/java/sonia/scm/work/Task.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializable task which can be enqueued to the {@link CentralWorkQueue}.
|
||||||
|
*
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
public interface Task extends Runnable, Serializable {
|
||||||
|
}
|
||||||
@@ -36,6 +36,24 @@ public class StoreConstants
|
|||||||
|
|
||||||
public static final String CONFIG_DIRECTORY_NAME = "config";
|
public static final String CONFIG_DIRECTORY_NAME = "config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the parent of data or blob directories.
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
public static final String VARIABLE_DATA_DIRECTORY_NAME = "var";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of data directories.
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
public static final String DATA_DIRECTORY_NAME = "data";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of blob directories.
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
public static final String BLOG_DIRECTORY_NAME = "data";
|
||||||
|
|
||||||
public static final String REPOSITORY_METADATA = "metadata";
|
public static final String REPOSITORY_METADATA = "metadata";
|
||||||
|
|
||||||
public static final String FILE_EXTENSION = ".xml";
|
public static final String FILE_EXTENSION = ".xml";
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
|
|
||||||
package sonia.scm.store;
|
package sonia.scm.store;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -59,7 +61,7 @@ public class InMemoryBlobStore implements BlobStore {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Blob> getAll() {
|
public List<Blob> getAll() {
|
||||||
return blobs;
|
return ImmutableList.copyOf(blobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import org.apache.shiro.authz.AuthorizationException;
|
import org.apache.shiro.authz.AuthorizationException;
|
||||||
import org.apache.shiro.authz.UnauthorizedException;
|
import org.apache.shiro.authz.UnauthorizedException;
|
||||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
|
||||||
import org.jboss.resteasy.spi.Dispatcher;
|
import org.jboss.resteasy.spi.Dispatcher;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.jboss.resteasy.spi.HttpResponse;
|
import org.jboss.resteasy.spi.HttpResponse;
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ const AvatarSection: FC<HitProps> = ({ hit }) => {
|
|||||||
const name = useStringHitFieldValue(hit, "name");
|
const name = useStringHitFieldValue(hit, "name");
|
||||||
const type = useStringHitFieldValue(hit, "type");
|
const type = useStringHitFieldValue(hit, "type");
|
||||||
|
|
||||||
const repository = hit._embedded.repository;
|
const repository = hit._embedded?.repository;
|
||||||
if (!namespace || !name || !type || !repository) {
|
if (!namespace || !name || !type || !repository) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const RepositoryHit: FC<HitProps> = ({ hit }) => {
|
|||||||
|
|
||||||
// the embedded repository is only a subset of the repository (RepositoryCoordinates),
|
// the embedded repository is only a subset of the repository (RepositoryCoordinates),
|
||||||
// so we should use the fields to get more information
|
// so we should use the fields to get more information
|
||||||
const repository = hit._embedded.repository;
|
const repository = hit._embedded?.repository;
|
||||||
if (!namespace || !name || !type || !repository) {
|
if (!namespace || !name || !type || !repository) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ import sonia.scm.plugin.Extension;
|
|||||||
import sonia.scm.search.HandlerEventIndexSyncer;
|
import sonia.scm.search.HandlerEventIndexSyncer;
|
||||||
import sonia.scm.search.Id;
|
import sonia.scm.search.Id;
|
||||||
import sonia.scm.search.Index;
|
import sonia.scm.search.Index;
|
||||||
|
import sonia.scm.search.IndexLogStore;
|
||||||
import sonia.scm.search.Indexer;
|
import sonia.scm.search.Indexer;
|
||||||
import sonia.scm.search.SearchEngine;
|
import sonia.scm.search.SearchEngine;
|
||||||
|
import sonia.scm.search.SerializableIndexTask;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -43,12 +45,10 @@ public class GroupIndexer implements Indexer<Group> {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final int VERSION = 1;
|
static final int VERSION = 1;
|
||||||
|
|
||||||
private final GroupManager groupManager;
|
|
||||||
private final SearchEngine searchEngine;
|
private final SearchEngine searchEngine;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GroupIndexer(GroupManager groupManager, SearchEngine searchEngine) {
|
public GroupIndexer(SearchEngine searchEngine) {
|
||||||
this.groupManager = groupManager;
|
|
||||||
this.searchEngine = searchEngine;
|
this.searchEngine = searchEngine;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,47 +62,47 @@ public class GroupIndexer implements Indexer<Group> {
|
|||||||
return VERSION;
|
return VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe(async = false)
|
@Override
|
||||||
public void handleEvent(GroupEvent event) {
|
public Class<? extends ReIndexAllTask<Group>> getReIndexAllTask() {
|
||||||
new HandlerEventIndexSyncer<>(this).handleEvent(event);
|
return ReIndexAll.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Updater<Group> open() {
|
public SerializableIndexTask<Group> createStoreTask(Group group) {
|
||||||
return new GroupIndexUpdater(groupManager, searchEngine.forType(Group.class).getOrCreate());
|
return index -> store(index, group);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GroupIndexUpdater implements Updater<Group> {
|
@Override
|
||||||
|
public SerializableIndexTask<Group> createDeleteTask(Group group) {
|
||||||
|
return index -> index.delete().byId(Id.of(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe(async = false)
|
||||||
|
public void handleEvent(GroupEvent event) {
|
||||||
|
new HandlerEventIndexSyncer<>(searchEngine, this).handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void store(Index<Group> index, Group group) {
|
||||||
|
index.store(Id.of(group), GroupPermissions.read(group).asShiroString(), group);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ReIndexAll extends ReIndexAllTask<Group> {
|
||||||
|
|
||||||
private final GroupManager groupManager;
|
private final GroupManager groupManager;
|
||||||
private final Index<Group> index;
|
|
||||||
|
|
||||||
private GroupIndexUpdater(GroupManager groupManager, Index<Group> index) {
|
@Inject
|
||||||
|
public ReIndexAll(IndexLogStore logStore, GroupManager groupManager) {
|
||||||
|
super(logStore, Group.class, VERSION);
|
||||||
this.groupManager = groupManager;
|
this.groupManager = groupManager;
|
||||||
this.index = index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void store(Group group) {
|
public void update(Index<Group> index) {
|
||||||
index.store(Id.of(group), GroupPermissions.read(group).asShiroString(), group);
|
index.delete().all();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(Group group) {
|
|
||||||
index.delete().byType().byId(Id.of(group));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reIndexAll() {
|
|
||||||
index.delete().byType().all();
|
|
||||||
for (Group group : groupManager.getAll()) {
|
for (Group group : groupManager.getAll()) {
|
||||||
store(group);
|
store(index, group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
index.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ import sonia.scm.web.cgi.DefaultCGIExecutorFactory;
|
|||||||
import sonia.scm.web.filter.LoggingFilter;
|
import sonia.scm.web.filter.LoggingFilter;
|
||||||
import sonia.scm.web.security.AdministrationContext;
|
import sonia.scm.web.security.AdministrationContext;
|
||||||
import sonia.scm.web.security.DefaultAdministrationContext;
|
import sonia.scm.web.security.DefaultAdministrationContext;
|
||||||
|
import sonia.scm.work.CentralWorkQueue;
|
||||||
|
import sonia.scm.work.DefaultCentralWorkQueue;
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.TrustManager;
|
import javax.net.ssl.TrustManager;
|
||||||
@@ -290,6 +292,8 @@ class ScmServletModule extends ServletModule {
|
|||||||
bind(SearchEngine.class, LuceneSearchEngine.class);
|
bind(SearchEngine.class, LuceneSearchEngine.class);
|
||||||
bind(IndexLogStore.class, DefaultIndexLogStore.class);
|
bind(IndexLogStore.class, DefaultIndexLogStore.class);
|
||||||
|
|
||||||
|
bind(CentralWorkQueue.class, DefaultCentralWorkQueue.class);
|
||||||
|
|
||||||
bind(ContentTypeResolver.class).to(DefaultContentTypeResolver.class);
|
bind(ContentTypeResolver.class).to(DefaultContentTypeResolver.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ package sonia.scm.repository;
|
|||||||
|
|
||||||
import com.github.legman.Subscribe;
|
import com.github.legman.Subscribe;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import sonia.scm.HandlerEventType;
|
||||||
import sonia.scm.plugin.Extension;
|
import sonia.scm.plugin.Extension;
|
||||||
import sonia.scm.search.HandlerEventIndexSyncer;
|
|
||||||
import sonia.scm.search.Id;
|
import sonia.scm.search.Id;
|
||||||
import sonia.scm.search.Index;
|
import sonia.scm.search.Index;
|
||||||
|
import sonia.scm.search.IndexLogStore;
|
||||||
import sonia.scm.search.Indexer;
|
import sonia.scm.search.Indexer;
|
||||||
import sonia.scm.search.SearchEngine;
|
import sonia.scm.search.SearchEngine;
|
||||||
|
import sonia.scm.search.SerializableIndexTask;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -43,12 +45,10 @@ public class RepositoryIndexer implements Indexer<Repository> {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final int VERSION = 3;
|
static final int VERSION = 3;
|
||||||
|
|
||||||
private final RepositoryManager repositoryManager;
|
|
||||||
private final SearchEngine searchEngine;
|
private final SearchEngine searchEngine;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public RepositoryIndexer(RepositoryManager repositoryManager, SearchEngine searchEngine) {
|
public RepositoryIndexer(SearchEngine searchEngine) {
|
||||||
this.repositoryManager = repositoryManager;
|
|
||||||
this.searchEngine = searchEngine;
|
this.searchEngine = searchEngine;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,49 +62,58 @@ public class RepositoryIndexer implements Indexer<Repository> {
|
|||||||
return Repository.class;
|
return Repository.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends ReIndexAllTask<Repository>> getReIndexAllTask() {
|
||||||
|
return ReIndexAll.class;
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe(async = false)
|
@Subscribe(async = false)
|
||||||
public void handleEvent(RepositoryEvent event) {
|
public void handleEvent(RepositoryEvent event) {
|
||||||
new HandlerEventIndexSyncer<>(this).handleEvent(event);
|
HandlerEventType type = event.getEventType();
|
||||||
|
if (type.isPost()) {
|
||||||
|
Repository repository = event.getItem();
|
||||||
|
if (type == HandlerEventType.DELETE) {
|
||||||
|
searchEngine.forIndices()
|
||||||
|
.forResource(repository)
|
||||||
|
.batch(createDeleteTask(repository));
|
||||||
|
} else {
|
||||||
|
searchEngine.forType(Repository.class)
|
||||||
|
.update(createStoreTask(repository));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Updater<Repository> open() {
|
public SerializableIndexTask<Repository> createStoreTask(Repository repository) {
|
||||||
return new RepositoryIndexUpdater(repositoryManager, searchEngine.forType(getType()).getOrCreate());
|
return index -> store(index, repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class RepositoryIndexUpdater implements Updater<Repository> {
|
@Override
|
||||||
|
public SerializableIndexTask<Repository> createDeleteTask(Repository repository) {
|
||||||
|
return index -> index.delete().byRepository(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void store(Index<Repository> index, Repository repository) {
|
||||||
|
index.store(Id.of(repository), RepositoryPermissions.read(repository).asShiroString(), repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ReIndexAll extends ReIndexAllTask<Repository> {
|
||||||
|
|
||||||
private final RepositoryManager repositoryManager;
|
private final RepositoryManager repositoryManager;
|
||||||
private final Index<Repository> index;
|
|
||||||
|
|
||||||
public RepositoryIndexUpdater(RepositoryManager repositoryManager, Index<Repository> index) {
|
@Inject
|
||||||
|
public ReIndexAll(IndexLogStore logStore, RepositoryManager repositoryManager) {
|
||||||
|
super(logStore, Repository.class, VERSION);
|
||||||
this.repositoryManager = repositoryManager;
|
this.repositoryManager = repositoryManager;
|
||||||
this.index = index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void store(Repository repository) {
|
public void update(Index<Repository> index) {
|
||||||
index.store(Id.of(repository), RepositoryPermissions.read(repository).asShiroString(), repository);
|
index.delete().all();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(Repository repository) {
|
|
||||||
index.delete().allTypes().byRepository(repository.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reIndexAll() {
|
|
||||||
// v1 used the whole classname as type
|
|
||||||
index.delete().allTypes().byTypeName(Repository.class.getName());
|
|
||||||
index.delete().byType().all();
|
|
||||||
for (Repository repository : repositoryManager.getAll()) {
|
for (Repository repository : repositoryManager.getAll()) {
|
||||||
store(repository);
|
store(index, repository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
index.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ package sonia.scm.search;
|
|||||||
final class FieldNames {
|
final class FieldNames {
|
||||||
private FieldNames(){}
|
private FieldNames(){}
|
||||||
|
|
||||||
static final String UID = "_uid";
|
|
||||||
static final String ID = "_id";
|
static final String ID = "_id";
|
||||||
static final String TYPE = "_type";
|
|
||||||
static final String REPOSITORY = "_repository";
|
static final String REPOSITORY = "_repository";
|
||||||
static final String PERMISSION = "_permission";
|
static final String PERMISSION = "_permission";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ package sonia.scm.search;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.plugin.Extension;
|
import sonia.scm.plugin.Extension;
|
||||||
import sonia.scm.web.security.AdministrationContext;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -43,13 +42,13 @@ public class IndexBootstrapListener implements ServletContextListener {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(IndexBootstrapListener.class);
|
private static final Logger LOG = LoggerFactory.getLogger(IndexBootstrapListener.class);
|
||||||
|
|
||||||
private final AdministrationContext administrationContext;
|
private final SearchEngine searchEngine;
|
||||||
private final IndexLogStore indexLogStore;
|
private final IndexLogStore indexLogStore;
|
||||||
private final Set<Indexer> indexers;
|
private final Set<Indexer> indexers;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public IndexBootstrapListener(AdministrationContext administrationContext, IndexLogStore indexLogStore, Set<Indexer> indexers) {
|
public IndexBootstrapListener(SearchEngine searchEngine, IndexLogStore indexLogStore, Set<Indexer> indexers) {
|
||||||
this.administrationContext = administrationContext;
|
this.searchEngine = searchEngine;
|
||||||
this.indexLogStore = indexLogStore;
|
this.indexLogStore = indexLogStore;
|
||||||
this.indexers = indexers;
|
this.indexers = indexers;
|
||||||
}
|
}
|
||||||
@@ -65,8 +64,11 @@ public class IndexBootstrapListener implements ServletContextListener {
|
|||||||
Optional<IndexLog> indexLog = indexLogStore.defaultIndex().get(indexer.getType());
|
Optional<IndexLog> indexLog = indexLogStore.defaultIndex().get(indexer.getType());
|
||||||
if (indexLog.isPresent()) {
|
if (indexLog.isPresent()) {
|
||||||
int version = indexLog.get().getVersion();
|
int version = indexLog.get().getVersion();
|
||||||
if (version < indexer.getVersion()) {
|
if (version != indexer.getVersion()) {
|
||||||
LOG.debug("index version {} is older then {}, start reindexing of all {}", version, indexer.getVersion(), indexer.getType());
|
LOG.debug(
|
||||||
|
"index version {} is older then {}, start reindexing of all {}",
|
||||||
|
version, indexer.getVersion(), indexer.getType()
|
||||||
|
);
|
||||||
indexAll(indexer);
|
indexAll(indexer);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -75,14 +77,9 @@ public class IndexBootstrapListener implements ServletContextListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private void indexAll(Indexer indexer) {
|
private void indexAll(Indexer indexer) {
|
||||||
administrationContext.runAsAdmin(() -> {
|
searchEngine.forType(indexer.getType()).update(indexer.getReIndexAllTask());
|
||||||
try (Indexer.Updater updater = indexer.open()) {
|
|
||||||
updater.reIndexAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
indexLogStore.defaultIndex().log(indexer.getType(), indexer.getVersion());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
139
scm-webapp/src/main/java/sonia/scm/search/IndexManager.java
Normal file
139
scm-webapp/src/main/java/sonia/scm/search/IndexManager.java
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.lucene.index.DirectoryReader;
|
||||||
|
import org.apache.lucene.index.IndexReader;
|
||||||
|
import org.apache.lucene.index.IndexWriter;
|
||||||
|
import org.apache.lucene.index.IndexWriterConfig;
|
||||||
|
import org.apache.lucene.store.FSDirectory;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import javax.xml.bind.JAXB;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class IndexManager {
|
||||||
|
|
||||||
|
private final Path directory;
|
||||||
|
private final AnalyzerFactory analyzerFactory;
|
||||||
|
private final IndexXml indexXml;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public IndexManager(SCMContextProvider context, PluginLoader pluginLoader, AnalyzerFactory analyzerFactory) {
|
||||||
|
directory = context.resolve(Paths.get("index"));
|
||||||
|
this.analyzerFactory = analyzerFactory;
|
||||||
|
this.indexXml = readIndexXml(pluginLoader.getUberClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexXml readIndexXml(ClassLoader uberClassLoader) {
|
||||||
|
Path path = directory.resolve("index.xml");
|
||||||
|
if (Files.exists(path)) {
|
||||||
|
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
|
||||||
|
try {
|
||||||
|
Thread.currentThread().setContextClassLoader(uberClassLoader);
|
||||||
|
return JAXB.unmarshal(path.toFile(), IndexXml.class);
|
||||||
|
} finally {
|
||||||
|
Thread.currentThread().setContextClassLoader(contextClassLoader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new IndexXml();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<? extends IndexDetails> all() {
|
||||||
|
return Collections.unmodifiableSet(indexXml.indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexReader openForRead(LuceneSearchableType type, String indexName) throws IOException {
|
||||||
|
Path path = resolveIndexDirectory(type, indexName);
|
||||||
|
return DirectoryReader.open(FSDirectory.open(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexWriter openForWrite(IndexParams indexParams) {
|
||||||
|
IndexWriterConfig config = new IndexWriterConfig(analyzerFactory.create(indexParams.getSearchableType(), indexParams.getOptions()));
|
||||||
|
config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
|
||||||
|
|
||||||
|
Path path = resolveIndexDirectory(indexParams);
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
store(new LuceneIndexDetails(indexParams.getType(), indexParams.getIndex()));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new IndexWriter(FSDirectory.open(path), config);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new SearchEngineException("failed to open index at " + path, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveIndexDirectory(IndexParams indexParams) {
|
||||||
|
return directory.resolve(indexParams.getSearchableType().getName()).resolve(indexParams.getIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveIndexDirectory(LuceneSearchableType searchableType, String indexName) {
|
||||||
|
return directory.resolve(searchableType.getName()).resolve(indexName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void store(LuceneIndexDetails details) {
|
||||||
|
if (!indexXml.getIndices().add(details)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.exists(directory)) {
|
||||||
|
try {
|
||||||
|
Files.createDirectory(directory);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new SearchEngineException("failed to create index directory", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path = directory.resolve("index.xml");
|
||||||
|
JAXB.marshal(indexXml, path.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@XmlRootElement(name = "indices")
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public static class IndexXml {
|
||||||
|
|
||||||
|
@XmlElement(name = "index")
|
||||||
|
private Set<LuceneIndexDetails> indices = new HashSet<>();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,10 +27,24 @@ package sonia.scm.search;
|
|||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
public class IndexParams {
|
public class IndexParams implements IndexDetails {
|
||||||
|
|
||||||
String index;
|
String index;
|
||||||
LuceneSearchableType searchableType;
|
LuceneSearchableType searchableType;
|
||||||
IndexOptions options;
|
IndexOptions options;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getType() {
|
||||||
|
return searchableType.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return searchableType.getName() + "/" + index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,54 +24,71 @@
|
|||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import org.apache.lucene.document.Document;
|
import org.apache.lucene.document.Document;
|
||||||
import org.apache.lucene.document.Field;
|
import org.apache.lucene.document.Field;
|
||||||
import org.apache.lucene.document.StringField;
|
import org.apache.lucene.document.StringField;
|
||||||
import org.apache.lucene.index.IndexWriter;
|
import org.apache.lucene.index.IndexWriter;
|
||||||
import org.apache.lucene.index.Term;
|
import org.apache.lucene.index.Term;
|
||||||
import org.apache.lucene.search.BooleanClause;
|
import org.slf4j.Logger;
|
||||||
import org.apache.lucene.search.BooleanQuery;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.apache.lucene.search.TermQuery;
|
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static sonia.scm.search.FieldNames.ID;
|
import static sonia.scm.search.FieldNames.ID;
|
||||||
import static sonia.scm.search.FieldNames.PERMISSION;
|
import static sonia.scm.search.FieldNames.PERMISSION;
|
||||||
import static sonia.scm.search.FieldNames.REPOSITORY;
|
import static sonia.scm.search.FieldNames.REPOSITORY;
|
||||||
import static sonia.scm.search.FieldNames.TYPE;
|
|
||||||
import static sonia.scm.search.FieldNames.UID;
|
|
||||||
|
|
||||||
class LuceneIndex<T> implements Index<T> {
|
class LuceneIndex<T> implements Index<T>, AutoCloseable {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(LuceneIndex.class);
|
||||||
|
|
||||||
|
private final IndexDetails details;
|
||||||
private final LuceneSearchableType searchableType;
|
private final LuceneSearchableType searchableType;
|
||||||
private final IndexWriter writer;
|
private final SharableIndexWriter writer;
|
||||||
|
|
||||||
LuceneIndex(LuceneSearchableType searchableType, IndexWriter writer) {
|
LuceneIndex(IndexParams params, Supplier<IndexWriter> writerFactory) {
|
||||||
this.searchableType = searchableType;
|
this.details = params;
|
||||||
this.writer = writer;
|
this.searchableType = params.getSearchableType();
|
||||||
|
this.writer = new SharableIndexWriter(writerFactory);
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
void open() {
|
||||||
|
writer.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
SharableIndexWriter getWriter() {
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IndexDetails getDetails() {
|
||||||
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void store(Id id, String permission, Object object) {
|
public void store(Id id, String permission, Object object) {
|
||||||
String uid = createUid(id, searchableType);
|
|
||||||
Document document = searchableType.getTypeConverter().convert(object);
|
Document document = searchableType.getTypeConverter().convert(object);
|
||||||
try {
|
try {
|
||||||
field(document, UID, uid);
|
field(document, ID, id.asString());
|
||||||
field(document, ID, id.getValue());
|
|
||||||
id.getRepository().ifPresent(repository -> field(document, REPOSITORY, repository));
|
id.getRepository().ifPresent(repository -> field(document, REPOSITORY, repository));
|
||||||
field(document, TYPE, searchableType.getName());
|
|
||||||
if (!Strings.isNullOrEmpty(permission)) {
|
if (!Strings.isNullOrEmpty(permission)) {
|
||||||
field(document, PERMISSION, permission);
|
field(document, PERMISSION, permission);
|
||||||
}
|
}
|
||||||
writer.updateDocument(new Term(UID, uid), document);
|
writer.updateDocument(idTerm(id), document);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new SearchEngineException("failed to add document to index", e);
|
throw new SearchEngineException("failed to add document to index", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createUid(Id id, LuceneSearchableType type) {
|
@Nonnull
|
||||||
return id.asString() + "/" + type.getName();
|
private Term idTerm(Id id) {
|
||||||
|
return new Term(ID, id.asString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void field(Document document, String type, String name) {
|
private void field(Document document, String type, String name) {
|
||||||
@@ -94,24 +111,11 @@ class LuceneIndex<T> implements Index<T> {
|
|||||||
|
|
||||||
private class LuceneDeleter implements Deleter {
|
private class LuceneDeleter implements Deleter {
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByTypeDeleter byType() {
|
|
||||||
return new LuceneByTypeDeleter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AllTypesDeleter allTypes() {
|
|
||||||
return new LuceneAllTypesDelete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("java:S1192")
|
|
||||||
private class LuceneByTypeDeleter implements ByTypeDeleter {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void byId(Id id) {
|
public void byId(Id id) {
|
||||||
try {
|
try {
|
||||||
writer.deleteDocuments(new Term(UID, createUid(id, searchableType)));
|
long count = writer.deleteDocuments(idTerm(id));
|
||||||
|
LOG.debug("delete {} document(s) by id {} from index {}", count, id, details);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new SearchEngineException("failed to delete document from index", e);
|
throw new SearchEngineException("failed to delete document from index", e);
|
||||||
}
|
}
|
||||||
@@ -120,7 +124,8 @@ class LuceneIndex<T> implements Index<T> {
|
|||||||
@Override
|
@Override
|
||||||
public void all() {
|
public void all() {
|
||||||
try {
|
try {
|
||||||
writer.deleteDocuments(new Term(TYPE, searchableType.getName()));
|
long count = writer.deleteAll();
|
||||||
|
LOG.debug("deleted all {} documents from index {}", count, details);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new SearchEngineException("failed to delete documents by type " + searchableType.getName() + " from index", ex);
|
throw new SearchEngineException("failed to delete documents by type " + searchableType.getName() + " from index", ex);
|
||||||
}
|
}
|
||||||
@@ -129,35 +134,16 @@ class LuceneIndex<T> implements Index<T> {
|
|||||||
@Override
|
@Override
|
||||||
public void byRepository(String repositoryId) {
|
public void byRepository(String repositoryId) {
|
||||||
try {
|
try {
|
||||||
BooleanQuery query = new BooleanQuery.Builder()
|
long count = writer.deleteDocuments(repositoryTerm(repositoryId));
|
||||||
.add(new TermQuery(new Term(TYPE, searchableType.getName())), BooleanClause.Occur.MUST)
|
LOG.debug("deleted {} documents by repository {} from index {}", count, repositoryId, details);
|
||||||
.add(new TermQuery(new Term(REPOSITORY, repositoryId)), BooleanClause.Occur.MUST)
|
|
||||||
.build();
|
|
||||||
writer.deleteDocuments(query);
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new SearchEngineException("failed to delete documents by repository " + repositoryId + " from index", ex);
|
throw new SearchEngineException("failed to delete documents by repository " + repositoryId + " from index", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class LuceneAllTypesDelete implements AllTypesDeleter {
|
@Nonnull
|
||||||
|
private Term repositoryTerm(String repositoryId) {
|
||||||
@Override
|
return new Term(REPOSITORY, repositoryId);
|
||||||
public void byRepository(String repositoryId) {
|
|
||||||
try {
|
|
||||||
writer.deleteDocuments(new Term(REPOSITORY, repositoryId));
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new SearchEngineException("failed to delete all documents by repository " + repositoryId + " from index", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void byTypeName(String typeName) {
|
|
||||||
try {
|
|
||||||
writer.deleteDocuments(new Term(TYPE, typeName));
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new SearchEngineException("failed to delete documents by type " + typeName + " from index", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@XmlRootElement
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class LuceneIndexDetails implements IndexDetails {
|
||||||
|
private Class<?> type;
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
@@ -24,23 +24,50 @@
|
|||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import lombok.AllArgsConstructor;
|
||||||
import java.io.IOException;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public class LuceneIndexFactory {
|
public class LuceneIndexFactory {
|
||||||
|
|
||||||
private final IndexOpener indexOpener;
|
private final IndexManager indexManager;
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private final Map<IndexKey, LuceneIndex> indexes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public LuceneIndexFactory(IndexOpener indexOpener) {
|
public LuceneIndexFactory(IndexManager indexManager) {
|
||||||
this.indexOpener = indexOpener;
|
this.indexManager = indexManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> LuceneIndex<T> create(IndexParams indexParams) {
|
public <T> LuceneIndex<T> create(IndexParams indexParams) {
|
||||||
try {
|
return indexes.compute(keyOf(indexParams), (key, index) -> {
|
||||||
return new LuceneIndex<>(indexParams.getSearchableType(), indexOpener.openForWrite(indexParams));
|
if (index != null) {
|
||||||
} catch (IOException ex) {
|
index.open();
|
||||||
throw new SearchEngineException("failed to open index " + indexParams.getIndex(), ex);
|
return index;
|
||||||
}
|
}
|
||||||
|
return new LuceneIndex<>(
|
||||||
|
indexParams,
|
||||||
|
() -> indexManager.openForWrite(indexParams)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexKey keyOf(IndexParams indexParams) {
|
||||||
|
return new IndexKey(
|
||||||
|
indexParams.getSearchableType().getName(), indexParams.getIndex()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@AllArgsConstructor
|
||||||
|
private static class IndexKey {
|
||||||
|
String type;
|
||||||
|
String name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public abstract class LuceneIndexTask implements Runnable, Serializable {
|
||||||
|
|
||||||
|
private final Class<?> type;
|
||||||
|
private final String indexName;
|
||||||
|
private final IndexOptions options;
|
||||||
|
|
||||||
|
private transient LuceneIndexFactory indexFactory;
|
||||||
|
private transient SearchableTypeResolver searchableTypeResolver;
|
||||||
|
private transient Injector injector;
|
||||||
|
|
||||||
|
protected LuceneIndexTask(IndexParams params) {
|
||||||
|
this.type = params.getSearchableType().getType();
|
||||||
|
this.indexName = params.getIndex();
|
||||||
|
this.options = params.getOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public void setIndexFactory(LuceneIndexFactory indexFactory) {
|
||||||
|
this.indexFactory = indexFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public void setSearchableTypeResolver(SearchableTypeResolver searchableTypeResolver) {
|
||||||
|
this.searchableTypeResolver = searchableTypeResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public void setInjector(Injector injector) {
|
||||||
|
this.injector = injector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract IndexTask<?> task(Injector injector);
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
public void run() {
|
||||||
|
LuceneSearchableType searchableType = searchableTypeResolver.resolve(type);
|
||||||
|
IndexTask<?> task = task(injector);
|
||||||
|
try (LuceneIndex index = indexFactory.create(new IndexParams(indexName, searchableType, options))) {
|
||||||
|
task.update(index);
|
||||||
|
}
|
||||||
|
task.afterUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -24,31 +24,21 @@
|
|||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import com.google.inject.Injector;
|
||||||
import org.slf4j.LoggerFactory;
|
import sonia.scm.work.Task;
|
||||||
|
|
||||||
public final class IndexQueueTaskWrapper<T> implements Runnable {
|
@SuppressWarnings("rawtypes")
|
||||||
|
public class LuceneInjectingIndexTask extends LuceneIndexTask implements Task {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(IndexQueueTaskWrapper.class);
|
private final Class<? extends IndexTask> taskClass;
|
||||||
|
|
||||||
private final LuceneIndexFactory indexFactory;
|
LuceneInjectingIndexTask(IndexParams params, Class<? extends IndexTask> taskClass) {
|
||||||
private final IndexParams indexParams;
|
super(params);
|
||||||
private final Iterable<IndexQueueTask<T>> tasks;
|
this.taskClass = taskClass;
|
||||||
|
|
||||||
IndexQueueTaskWrapper(LuceneIndexFactory indexFactory, IndexParams indexParams, Iterable<IndexQueueTask<T>> tasks) {
|
|
||||||
this.indexFactory = indexFactory;
|
|
||||||
this.indexParams = indexParams;
|
|
||||||
this.tasks = tasks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public IndexTask<?> task(Injector injector) {
|
||||||
try (Index<T> index = indexFactory.create(indexParams)) {
|
return injector.getInstance(taskClass);
|
||||||
for (IndexQueueTask<T> task : tasks) {
|
|
||||||
task.updateIndex(index);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warn("failure during execution of index task for index {}", indexParams.getIndex(), e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,12 +54,12 @@ public class LuceneQueryBuilder<T> extends QueryBuilder<T> {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(LuceneQueryBuilder.class);
|
private static final Logger LOG = LoggerFactory.getLogger(LuceneQueryBuilder.class);
|
||||||
|
|
||||||
private final IndexOpener opener;
|
private final IndexManager opener;
|
||||||
private final LuceneSearchableType searchableType;
|
private final LuceneSearchableType searchableType;
|
||||||
private final String indexName;
|
private final String indexName;
|
||||||
private final Analyzer analyzer;
|
private final Analyzer analyzer;
|
||||||
|
|
||||||
LuceneQueryBuilder(IndexOpener opener, String indexName, LuceneSearchableType searchableType, Analyzer analyzer) {
|
LuceneQueryBuilder(IndexManager opener, String indexName, LuceneSearchableType searchableType, Analyzer analyzer) {
|
||||||
this.opener = opener;
|
this.opener = opener;
|
||||||
this.indexName = indexName;
|
this.indexName = indexName;
|
||||||
this.searchableType = searchableType;
|
this.searchableType = searchableType;
|
||||||
@@ -88,11 +88,11 @@ public class LuceneQueryBuilder<T> extends QueryBuilder<T> {
|
|||||||
String queryString = Strings.nullToEmpty(queryParams.getQueryString());
|
String queryString = Strings.nullToEmpty(queryParams.getQueryString());
|
||||||
|
|
||||||
Query parsedQuery = createQuery(searchableType, queryParams, queryString);
|
Query parsedQuery = createQuery(searchableType, queryParams, queryString);
|
||||||
Query query = Queries.filter(parsedQuery, searchableType, queryParams);
|
Query query = Queries.filter(parsedQuery, queryParams);
|
||||||
if (LOG.isDebugEnabled()) {
|
if (LOG.isDebugEnabled()) {
|
||||||
LOG.debug("execute lucene query: {}", query);
|
LOG.debug("execute lucene query: {}", query);
|
||||||
}
|
}
|
||||||
try (IndexReader reader = opener.openForRead(indexName)) {
|
try (IndexReader reader = opener.openForRead(searchableType, indexName)) {
|
||||||
IndexSearcher searcher = new IndexSearcher(reader);
|
IndexSearcher searcher = new IndexSearcher(reader);
|
||||||
|
|
||||||
searcher.search(query, new PermissionAwareCollector(reader, collector));
|
searcher.search(query, new PermissionAwareCollector(reader, collector));
|
||||||
|
|||||||
@@ -28,18 +28,18 @@ import javax.inject.Inject;
|
|||||||
|
|
||||||
public class LuceneQueryBuilderFactory {
|
public class LuceneQueryBuilderFactory {
|
||||||
|
|
||||||
private final IndexOpener indexOpener;
|
private final IndexManager indexManager;
|
||||||
private final AnalyzerFactory analyzerFactory;
|
private final AnalyzerFactory analyzerFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public LuceneQueryBuilderFactory(IndexOpener indexOpener, AnalyzerFactory analyzerFactory) {
|
public LuceneQueryBuilderFactory(IndexManager indexManager, AnalyzerFactory analyzerFactory) {
|
||||||
this.indexOpener = indexOpener;
|
this.indexManager = indexManager;
|
||||||
this.analyzerFactory = analyzerFactory;
|
this.analyzerFactory = analyzerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> LuceneQueryBuilder<T> create(IndexParams indexParams) {
|
public <T> LuceneQueryBuilder<T> create(IndexParams indexParams) {
|
||||||
return new LuceneQueryBuilder<>(
|
return new LuceneQueryBuilder<>(
|
||||||
indexOpener,
|
indexManager,
|
||||||
indexParams.getIndex(),
|
indexParams.getIndex(),
|
||||||
indexParams.getSearchableType(),
|
indexParams.getSearchableType(),
|
||||||
analyzerFactory.create(indexParams.getSearchableType(), indexParams.getOptions())
|
analyzerFactory.create(indexParams.getSearchableType(), indexParams.getOptions())
|
||||||
|
|||||||
@@ -24,24 +24,34 @@
|
|||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import sonia.scm.work.CentralWorkQueue;
|
||||||
|
import sonia.scm.work.CentralWorkQueue.Enqueue;
|
||||||
|
import sonia.scm.work.Task;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class LuceneSearchEngine implements SearchEngine {
|
public class LuceneSearchEngine implements SearchEngine {
|
||||||
|
|
||||||
|
private final IndexManager indexManager;
|
||||||
private final SearchableTypeResolver resolver;
|
private final SearchableTypeResolver resolver;
|
||||||
private final IndexQueue indexQueue;
|
|
||||||
private final LuceneQueryBuilderFactory queryBuilderFactory;
|
private final LuceneQueryBuilderFactory queryBuilderFactory;
|
||||||
|
private final CentralWorkQueue centralWorkQueue;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public LuceneSearchEngine(SearchableTypeResolver resolver, IndexQueue indexQueue, LuceneQueryBuilderFactory queryBuilderFactory) {
|
public LuceneSearchEngine(IndexManager indexManager, SearchableTypeResolver resolver, LuceneQueryBuilderFactory queryBuilderFactory, CentralWorkQueue centralWorkQueue) {
|
||||||
|
this.indexManager = indexManager;
|
||||||
this.resolver = resolver;
|
this.resolver = resolver;
|
||||||
this.indexQueue = indexQueue;
|
|
||||||
this.queryBuilderFactory = queryBuilderFactory;
|
this.queryBuilderFactory = queryBuilderFactory;
|
||||||
|
this.centralWorkQueue = centralWorkQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -67,11 +77,79 @@ public class LuceneSearchEngine implements SearchEngine {
|
|||||||
return new LuceneForType<>(searchableType);
|
return new LuceneForType<>(searchableType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void enqueue(LuceneSearchableType searchableType, String index, List<String> resources, Task task) {
|
||||||
|
Enqueue enqueuer = centralWorkQueue.append();
|
||||||
|
|
||||||
|
String resourceName = Joiner.on('-').join(searchableType.getName(), index, "index");
|
||||||
|
if (resources.isEmpty()) {
|
||||||
|
enqueuer.locks(resourceName);
|
||||||
|
} else {
|
||||||
|
for (String resource : resources) {
|
||||||
|
enqueuer.locks(resourceName, resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueuer.runAsAdmin().enqueue(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ForIndices forIndices() {
|
||||||
|
return new LuceneForIndices();
|
||||||
|
}
|
||||||
|
|
||||||
|
class LuceneForIndices implements ForIndices {
|
||||||
|
|
||||||
|
private final List<String> resources = new ArrayList<>();
|
||||||
|
private Predicate<IndexDetails> predicate = details -> true;
|
||||||
|
private IndexOptions options = IndexOptions.defaults();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ForIndices matching(Predicate<IndexDetails> predicate) {
|
||||||
|
this.predicate = predicate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ForIndices withOptions(IndexOptions options) {
|
||||||
|
this.options = options;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ForIndices forResource(String resource) {
|
||||||
|
this.resources.add(resource);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void batch(SerializableIndexTask<?> task) {
|
||||||
|
exec(params -> batch(params, new LuceneSimpleIndexTask(params, task)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void batch(Class<? extends IndexTask<?>> task) {
|
||||||
|
exec(params -> batch(params, new LuceneInjectingIndexTask(params, task)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exec(Consumer<IndexParams> consumer) {
|
||||||
|
indexManager.all()
|
||||||
|
.stream()
|
||||||
|
.filter(predicate)
|
||||||
|
.map(details -> new IndexParams(details.getName(), resolver.resolve(details.getType()), options))
|
||||||
|
.forEach(consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void batch(IndexParams params, Task task) {
|
||||||
|
LuceneSearchEngine.this.enqueue(params.getSearchableType(), params.getIndex(), resources, task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class LuceneForType<T> implements ForType<T> {
|
class LuceneForType<T> implements ForType<T> {
|
||||||
|
|
||||||
private final LuceneSearchableType searchableType;
|
private final LuceneSearchableType searchableType;
|
||||||
private IndexOptions options = IndexOptions.defaults();
|
private IndexOptions options = IndexOptions.defaults();
|
||||||
private String index = "default";
|
private String index = "default";
|
||||||
|
private final List<String> resources = new ArrayList<>();
|
||||||
|
|
||||||
private LuceneForType(LuceneSearchableType searchableType) {
|
private LuceneForType(LuceneSearchableType searchableType) {
|
||||||
this.searchableType = searchableType;
|
this.searchableType = searchableType;
|
||||||
@@ -94,8 +172,23 @@ public class LuceneSearchEngine implements SearchEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Index<T> getOrCreate() {
|
public ForType<T> forResource(String resource) {
|
||||||
return indexQueue.getQueuedIndex(params());
|
resources.add(resource);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(Class<? extends IndexTask<T>> task) {
|
||||||
|
enqueue(new LuceneInjectingIndexTask(params(), task));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(SerializableIndexTask<T> task) {
|
||||||
|
enqueue(new LuceneSimpleIndexTask(params(), task));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enqueue(Task task) {
|
||||||
|
LuceneSearchEngine.this.enqueue(searchableType, index, resources, task);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -24,53 +24,21 @@
|
|||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.inject.Injector;
|
||||||
|
import sonia.scm.work.Task;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
public final class LuceneSimpleIndexTask extends LuceneIndexTask implements Task {
|
||||||
import javax.inject.Singleton;
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
|
|
||||||
@Singleton
|
private final SerializableIndexTask<?> task;
|
||||||
public class IndexQueue implements Closeable {
|
|
||||||
|
|
||||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
LuceneSimpleIndexTask(IndexParams params, SerializableIndexTask<?> task) {
|
||||||
|
super(params);
|
||||||
private final AtomicLong size = new AtomicLong(0);
|
this.task = task;
|
||||||
|
|
||||||
private final LuceneIndexFactory indexFactory;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public IndexQueue(LuceneIndexFactory indexFactory) {
|
|
||||||
this.indexFactory = indexFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> Index<T> getQueuedIndex(IndexParams indexParams) {
|
|
||||||
return new QueuedIndex<>(this, indexParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LuceneIndexFactory getIndexFactory() {
|
|
||||||
return indexFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
<T> void enqueue(IndexQueueTaskWrapper<T> task) {
|
|
||||||
size.incrementAndGet();
|
|
||||||
executor.execute(() -> {
|
|
||||||
task.run();
|
|
||||||
size.decrementAndGet();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
long getSize() {
|
|
||||||
return size.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public IndexTask<?> task(Injector injector) {
|
||||||
executor.shutdown();
|
injector.injectMembers(task);
|
||||||
|
return task;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,8 @@ import org.apache.lucene.search.BooleanQuery;
|
|||||||
import org.apache.lucene.search.Query;
|
import org.apache.lucene.search.Query;
|
||||||
import org.apache.lucene.search.TermQuery;
|
import org.apache.lucene.search.TermQuery;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.apache.lucene.search.BooleanClause.Occur.MUST;
|
import static org.apache.lucene.search.BooleanClause.Occur.MUST;
|
||||||
|
|
||||||
final class Queries {
|
final class Queries {
|
||||||
@@ -36,19 +38,18 @@ final class Queries {
|
|||||||
private Queries() {
|
private Queries() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Query typeQuery(LuceneSearchableType type) {
|
|
||||||
return new TermQuery(new Term(FieldNames.TYPE, type.getName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Query repositoryQuery(String repositoryId) {
|
private static Query repositoryQuery(String repositoryId) {
|
||||||
return new TermQuery(new Term(FieldNames.REPOSITORY, repositoryId));
|
return new TermQuery(new Term(FieldNames.REPOSITORY, repositoryId));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Query filter(Query query, LuceneSearchableType searchableType, QueryBuilder.QueryParams params) {
|
static Query filter(Query query, QueryBuilder.QueryParams params) {
|
||||||
BooleanQuery.Builder builder = new BooleanQuery.Builder()
|
Optional<String> repositoryId = params.getRepositoryId();
|
||||||
.add(query, MUST)
|
if (repositoryId.isPresent()) {
|
||||||
.add(typeQuery(searchableType), MUST);
|
return new BooleanQuery.Builder()
|
||||||
params.getRepositoryId().ifPresent(repo -> builder.add(repositoryQuery(repo), MUST));
|
.add(query, MUST)
|
||||||
return builder.build();
|
.add(repositoryQuery(repositoryId.get()), MUST)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sonia.scm.search;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
class QueuedIndex<T> implements Index<T> {
|
|
||||||
|
|
||||||
private final IndexQueue queue;
|
|
||||||
private final IndexParams indexParams;
|
|
||||||
private final List<IndexQueueTask<T>> tasks = new ArrayList<>();
|
|
||||||
|
|
||||||
QueuedIndex(IndexQueue queue, IndexParams indexParams) {
|
|
||||||
this.queue = queue;
|
|
||||||
this.indexParams = indexParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void store(Id id, String permission, T object) {
|
|
||||||
tasks.add(index -> index.store(id, permission, object));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Deleter delete() {
|
|
||||||
return new QueueDeleter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
IndexQueueTaskWrapper<T> wrappedTask = new IndexQueueTaskWrapper<>(
|
|
||||||
queue.getIndexFactory(), indexParams, tasks
|
|
||||||
);
|
|
||||||
queue.enqueue(wrappedTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class QueueDeleter implements Deleter {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByTypeDeleter byType() {
|
|
||||||
return new QueueByTypeDeleter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AllTypesDeleter allTypes() {
|
|
||||||
return new QueueAllTypesDeleter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class QueueByTypeDeleter implements ByTypeDeleter {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void byId(Id id) {
|
|
||||||
tasks.add(index -> index.delete().byType().byId(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void all() {
|
|
||||||
tasks.add(index -> index.delete().byType().all());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void byRepository(String repositoryId) {
|
|
||||||
tasks.add(index -> index.delete().byType().byRepository(repositoryId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class QueueAllTypesDeleter implements AllTypesDeleter {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void byRepository(String repositoryId) {
|
|
||||||
tasks.add(index -> index.delete().allTypes().byRepository(repositoryId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void byTypeName(String typeName) {
|
|
||||||
tasks.add(index -> index.delete().allTypes().byTypeName(typeName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.apache.lucene.document.Document;
|
||||||
|
import org.apache.lucene.index.IndexWriter;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
class SharableIndexWriter {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SharableIndexWriter.class);
|
||||||
|
|
||||||
|
private int usageCounter = 0;
|
||||||
|
|
||||||
|
private final Supplier<IndexWriter> writerFactory;
|
||||||
|
private IndexWriter writer;
|
||||||
|
|
||||||
|
SharableIndexWriter(Supplier<IndexWriter> writerFactory) {
|
||||||
|
this.writerFactory = writerFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void open() {
|
||||||
|
usageCounter++;
|
||||||
|
if (usageCounter == 1) {
|
||||||
|
LOG.trace("open writer, because usage increased from zero to one");
|
||||||
|
writer = writerFactory.get();
|
||||||
|
} else {
|
||||||
|
LOG.trace("new task is using the writer, counter is now at {}", usageCounter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
int getUsageCounter() {
|
||||||
|
return usageCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateDocument(Term term, Document document) throws IOException {
|
||||||
|
writer.updateDocument(term, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
long deleteDocuments(Term term) throws IOException {
|
||||||
|
return writer.deleteDocuments(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
long deleteAll() throws IOException {
|
||||||
|
return writer.deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void close() throws IOException {
|
||||||
|
usageCounter--;
|
||||||
|
if (usageCounter == 0) {
|
||||||
|
LOG.trace("no one seems to use index any longer, closing underlying writer");
|
||||||
|
writer.close();
|
||||||
|
writer = null;
|
||||||
|
} else if (usageCounter > 0) {
|
||||||
|
LOG.trace("index is still used by {} task(s), commit work but keep writer open", usageCounter);
|
||||||
|
writer.commit();
|
||||||
|
} else {
|
||||||
|
LOG.warn("index is already closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
scm-webapp/src/main/java/sonia/scm/security/Impersonator.java
Normal file
128
scm-webapp/src/main/java/sonia/scm/security/Impersonator.java
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.mgt.SecurityManager;
|
||||||
|
import org.apache.shiro.subject.PrincipalCollection;
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.apache.shiro.subject.support.SubjectThreadState;
|
||||||
|
import org.apache.shiro.util.ThreadContext;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Impersonator allows the usage of scm-manager api in the context of another user.
|
||||||
|
*
|
||||||
|
* @since 2.23.0
|
||||||
|
*/
|
||||||
|
public final class Impersonator {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Impersonator.class);
|
||||||
|
|
||||||
|
private final SecurityManager securityManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public Impersonator(SecurityManager securityManager) {
|
||||||
|
this.securityManager = securityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Session impersonate(PrincipalCollection principal) {
|
||||||
|
Subject subject = createSubject(principal);
|
||||||
|
if (ThreadContext.getSecurityManager() != null) {
|
||||||
|
return new WebImpersonator(subject);
|
||||||
|
}
|
||||||
|
return new NonWebImpersonator(securityManager, subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Subject createSubject(PrincipalCollection principal) {
|
||||||
|
return new Subject.Builder(securityManager)
|
||||||
|
.authenticated(true)
|
||||||
|
.principals(principal)
|
||||||
|
.buildSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Session extends AutoCloseable {
|
||||||
|
void close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class WebImpersonator implements Session {
|
||||||
|
|
||||||
|
private final Subject subject;
|
||||||
|
private final Subject previousSubject;
|
||||||
|
|
||||||
|
private WebImpersonator(Subject subject) {
|
||||||
|
this.subject = subject;
|
||||||
|
this.previousSubject = SecurityUtils.getSubject();
|
||||||
|
bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bind() {
|
||||||
|
LOG.debug("user {} start impersonate session as {}", previousSubject.getPrincipal(), subject.getPrincipal());
|
||||||
|
|
||||||
|
|
||||||
|
// do not use runas, because we want only bind the session to this thread.
|
||||||
|
// Runas could affect other threads.
|
||||||
|
ThreadContext.bind(this.subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
LOG.debug("release impersonate session from user {} to {}", previousSubject.getPrincipal(), subject.getPrincipal());
|
||||||
|
ThreadContext.bind(previousSubject);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NonWebImpersonator implements Session {
|
||||||
|
|
||||||
|
private final SecurityManager securityManager;
|
||||||
|
private final SubjectThreadState state;
|
||||||
|
private final Subject subject;
|
||||||
|
|
||||||
|
private NonWebImpersonator(SecurityManager securityManager, Subject subject) {
|
||||||
|
this.securityManager = securityManager;
|
||||||
|
this.state = new SubjectThreadState(subject);
|
||||||
|
this.subject = subject;
|
||||||
|
bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bind() {
|
||||||
|
LOG.debug("start impersonate session as user {}", subject.getPrincipal());
|
||||||
|
SecurityUtils.setSecurityManager(securityManager);
|
||||||
|
state.bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
LOG.debug("release impersonate session of {}", subject.getPrincipal());
|
||||||
|
state.restore();
|
||||||
|
SecurityUtils.setSecurityManager(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,44 +22,59 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.update.index;
|
||||||
|
|
||||||
import org.apache.lucene.index.DirectoryReader;
|
|
||||||
import org.apache.lucene.index.IndexReader;
|
|
||||||
import org.apache.lucene.index.IndexWriter;
|
|
||||||
import org.apache.lucene.index.IndexWriterConfig;
|
|
||||||
import org.apache.lucene.store.Directory;
|
|
||||||
import org.apache.lucene.store.FSDirectory;
|
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.migration.UpdateStep;
|
||||||
|
import sonia.scm.plugin.Extension;
|
||||||
|
import sonia.scm.util.IOUtil;
|
||||||
|
import sonia.scm.version.Version;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
public class IndexOpener {
|
import static sonia.scm.store.StoreConstants.DATA_DIRECTORY_NAME;
|
||||||
|
import static sonia.scm.store.StoreConstants.VARIABLE_DATA_DIRECTORY_NAME;
|
||||||
|
|
||||||
private final Path directory;
|
@Extension
|
||||||
private final AnalyzerFactory analyzerFactory;
|
public class RemoveCombinedIndex implements UpdateStep {
|
||||||
|
|
||||||
|
private final SCMContextProvider contextProvider;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public IndexOpener(SCMContextProvider context, AnalyzerFactory analyzerFactory) {
|
public RemoveCombinedIndex(SCMContextProvider contextProvider) {
|
||||||
directory = context.resolve(Paths.get("index"));
|
this.contextProvider = contextProvider;
|
||||||
this.analyzerFactory = analyzerFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IndexReader openForRead(String name) throws IOException {
|
@Override
|
||||||
return DirectoryReader.open(directory(name));
|
public void doUpdate() throws IOException {
|
||||||
|
Path index = contextProvider.resolve(Paths.get("index"));
|
||||||
|
if (Files.exists(index)) {
|
||||||
|
IOUtil.delete(index.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
Path indexLog = contextProvider.resolve(indexLogPath());
|
||||||
|
if (Files.exists(indexLog)) {
|
||||||
|
IOUtil.delete(indexLog.toFile());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IndexWriter openForWrite(IndexParams indexParams) throws IOException {
|
@Nonnull
|
||||||
IndexWriterConfig config = new IndexWriterConfig(analyzerFactory.create(indexParams.getSearchableType(), indexParams.getOptions()));
|
private Path indexLogPath() {
|
||||||
config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
|
return Paths.get(VARIABLE_DATA_DIRECTORY_NAME).resolve(DATA_DIRECTORY_NAME).resolve("index-log");
|
||||||
return new IndexWriter(directory(indexParams.getIndex()), config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Directory directory(String name) throws IOException {
|
@Override
|
||||||
return FSDirectory.open(directory.resolve(name));
|
public Version getTargetVersion() {
|
||||||
|
return Version.parse("2.0.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffectedDataType() {
|
||||||
|
return "sonia.scm.index";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -30,8 +30,10 @@ import sonia.scm.plugin.Extension;
|
|||||||
import sonia.scm.search.HandlerEventIndexSyncer;
|
import sonia.scm.search.HandlerEventIndexSyncer;
|
||||||
import sonia.scm.search.Id;
|
import sonia.scm.search.Id;
|
||||||
import sonia.scm.search.Index;
|
import sonia.scm.search.Index;
|
||||||
|
import sonia.scm.search.IndexLogStore;
|
||||||
import sonia.scm.search.Indexer;
|
import sonia.scm.search.Indexer;
|
||||||
import sonia.scm.search.SearchEngine;
|
import sonia.scm.search.SearchEngine;
|
||||||
|
import sonia.scm.search.SerializableIndexTask;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -43,12 +45,10 @@ public class UserIndexer implements Indexer<User> {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final int VERSION = 1;
|
static final int VERSION = 1;
|
||||||
|
|
||||||
private final UserManager userManager;
|
|
||||||
private final SearchEngine searchEngine;
|
private final SearchEngine searchEngine;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public UserIndexer(UserManager userManager, SearchEngine searchEngine) {
|
public UserIndexer(SearchEngine searchEngine) {
|
||||||
this.userManager = userManager;
|
|
||||||
this.searchEngine = searchEngine;
|
this.searchEngine = searchEngine;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,47 +62,46 @@ public class UserIndexer implements Indexer<User> {
|
|||||||
return VERSION;
|
return VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe(async = false)
|
@Override
|
||||||
public void handleEvent(UserEvent event) {
|
public Class<? extends ReIndexAllTask<User>> getReIndexAllTask() {
|
||||||
new HandlerEventIndexSyncer<>(this).handleEvent(event);
|
return ReIndexAll.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Updater<User> open() {
|
public SerializableIndexTask<User> createStoreTask(User user) {
|
||||||
return new UserIndexUpdater(userManager, searchEngine.forType(User.class).getOrCreate());
|
return index -> store(index, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class UserIndexUpdater implements Updater<User> {
|
@Override
|
||||||
|
public SerializableIndexTask<User> createDeleteTask(User item) {
|
||||||
|
return index -> index.delete().byId(Id.of(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe(async = false)
|
||||||
|
public void handleEvent(UserEvent event) {
|
||||||
|
new HandlerEventIndexSyncer<>(searchEngine, this).handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void store(Index<User> index, User user) {
|
||||||
|
index.store(Id.of(user), UserPermissions.read(user).asShiroString(), user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ReIndexAll extends ReIndexAllTask<User> {
|
||||||
|
|
||||||
private final UserManager userManager;
|
private final UserManager userManager;
|
||||||
private final Index<User> index;
|
|
||||||
|
|
||||||
private UserIndexUpdater(UserManager userManager, Index<User> index) {
|
@Inject
|
||||||
|
public ReIndexAll(IndexLogStore logStore, UserManager userManager) {
|
||||||
|
super(logStore, User.class, VERSION);
|
||||||
this.userManager = userManager;
|
this.userManager = userManager;
|
||||||
this.index = index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void store(User user) {
|
public void update(Index<User> index) {
|
||||||
index.store(Id.of(user), UserPermissions.read(user).asShiroString(), user);
|
index.delete().all();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(User user) {
|
|
||||||
index.delete().byType().byId(Id.of(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reIndexAll() {
|
|
||||||
index.delete().byType().all();
|
|
||||||
for (User user : userManager.getAll()) {
|
for (User user : userManager.getAll()) {
|
||||||
store(user);
|
store(index, user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
index.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,9 @@
|
|||||||
|
|
||||||
package sonia.scm.web.security;
|
package sonia.scm.web.security;
|
||||||
|
|
||||||
final class AdministrationContextMarker {
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
final class AdministrationContextMarker implements Serializable {
|
||||||
|
|
||||||
static final AdministrationContextMarker MARKER = new AdministrationContextMarker();
|
static final AdministrationContextMarker MARKER = new AdministrationContextMarker();
|
||||||
|
|
||||||
|
|||||||
@@ -24,223 +24,70 @@
|
|||||||
|
|
||||||
package sonia.scm.web.security;
|
package sonia.scm.web.security;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.apache.shiro.subject.PrincipalCollection;
|
import org.apache.shiro.subject.PrincipalCollection;
|
||||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||||
import org.apache.shiro.subject.Subject;
|
|
||||||
import org.apache.shiro.subject.support.SubjectThreadState;
|
|
||||||
import org.apache.shiro.util.ThreadContext;
|
|
||||||
import org.apache.shiro.util.ThreadState;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.SCMContext;
|
|
||||||
import sonia.scm.security.Authentications;
|
import sonia.scm.security.Authentications;
|
||||||
import sonia.scm.security.Role;
|
import sonia.scm.security.Impersonator;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.util.AssertUtil;
|
import sonia.scm.util.AssertUtil;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
public class DefaultAdministrationContext implements AdministrationContext
|
public class DefaultAdministrationContext implements AdministrationContext {
|
||||||
{
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final User SYSTEM_ACCOUNT = new User(
|
private static final User SYSTEM_ACCOUNT = new User(
|
||||||
Authentications.PRINCIPAL_SYSTEM,
|
Authentications.PRINCIPAL_SYSTEM,
|
||||||
"SCM-Manager System Account",
|
"SCM-Manager System Account",
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
static final String REALM = "AdminRealm";
|
static final String REALM = "AdminRealm";
|
||||||
|
|
||||||
/** the logger for DefaultAdministrationContext */
|
private static final Logger LOG = LoggerFactory.getLogger(DefaultAdministrationContext.class);
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(DefaultAdministrationContext.class);
|
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
private final Injector injector;
|
||||||
|
private final Impersonator impersonator;
|
||||||
|
private final PrincipalCollection adminPrincipal;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs ...
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param injector
|
|
||||||
* @param securityManager
|
|
||||||
*/
|
|
||||||
@Inject
|
@Inject
|
||||||
public DefaultAdministrationContext(Injector injector,
|
public DefaultAdministrationContext(Injector injector, Impersonator impersonator) {
|
||||||
org.apache.shiro.mgt.SecurityManager securityManager)
|
|
||||||
{
|
|
||||||
this.injector = injector;
|
this.injector = injector;
|
||||||
this.securityManager = securityManager;
|
this.impersonator = impersonator;
|
||||||
|
this.adminPrincipal = createAdminPrincipal();
|
||||||
principalCollection = createAdminCollection(SYSTEM_ACCOUNT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
public static PrincipalCollection createAdminPrincipal() {
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param action
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void runAsAdmin(PrivilegedAction action)
|
|
||||||
{
|
|
||||||
AssertUtil.assertIsNotNull(action);
|
|
||||||
|
|
||||||
if (ThreadContext.getSecurityManager() != null)
|
|
||||||
{
|
|
||||||
doRunAsInWebSessionContext(action);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
doRunAsInNonWebSessionContext(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param actionClass
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void runAsAdmin(Class<? extends PrivilegedAction> actionClass)
|
|
||||||
{
|
|
||||||
PrivilegedAction action = injector.getInstance(actionClass);
|
|
||||||
|
|
||||||
runAsAdmin(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param adminUser
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private PrincipalCollection createAdminCollection(User adminUser)
|
|
||||||
{
|
|
||||||
SimplePrincipalCollection collection = new SimplePrincipalCollection();
|
SimplePrincipalCollection collection = new SimplePrincipalCollection();
|
||||||
|
|
||||||
collection.add(adminUser.getId(), REALM);
|
collection.add(SYSTEM_ACCOUNT.getId(), REALM);
|
||||||
collection.add(adminUser, REALM);
|
collection.add(SYSTEM_ACCOUNT, REALM);
|
||||||
collection.add(AdministrationContextMarker.MARKER, REALM);
|
collection.add(AdministrationContextMarker.MARKER, REALM);
|
||||||
|
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Method description
|
public void runAsAdmin(PrivilegedAction action) {
|
||||||
*
|
AssertUtil.assertIsNotNull(action);
|
||||||
*
|
LOG.debug("execute action {} in administration context", action.getClass().getName());
|
||||||
* @return
|
try (Impersonator.Session session = impersonator.impersonate(adminPrincipal)) {
|
||||||
*/
|
|
||||||
private Subject createAdminSubject()
|
|
||||||
{
|
|
||||||
//J-
|
|
||||||
return new Subject.Builder(securityManager)
|
|
||||||
.authenticated(true)
|
|
||||||
.principals(principalCollection)
|
|
||||||
.buildSubject();
|
|
||||||
//J+
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doRunAsInNonWebSessionContext(PrivilegedAction action) {
|
|
||||||
logger.trace("bind shiro security manager to current thread");
|
|
||||||
|
|
||||||
try {
|
|
||||||
SecurityUtils.setSecurityManager(securityManager);
|
|
||||||
|
|
||||||
Subject subject = createAdminSubject();
|
|
||||||
ThreadState state = new SubjectThreadState(subject);
|
|
||||||
|
|
||||||
state.bind();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
logger.debug("execute action {} in administration context", action.getClass().getName());
|
|
||||||
|
|
||||||
action.run();
|
|
||||||
} finally {
|
|
||||||
logger.trace("restore current thread state");
|
|
||||||
state.restore();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
SecurityUtils.setSecurityManager(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param action
|
|
||||||
*/
|
|
||||||
private void doRunAsInWebSessionContext(PrivilegedAction action)
|
|
||||||
{
|
|
||||||
Subject subject = SecurityUtils.getSubject();
|
|
||||||
|
|
||||||
String principal = (String) subject.getPrincipal();
|
|
||||||
|
|
||||||
if (logger.isInfoEnabled())
|
|
||||||
{
|
|
||||||
String username;
|
|
||||||
|
|
||||||
if (subject.hasRole(Role.USER))
|
|
||||||
{
|
|
||||||
username = principal;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
username = SCMContext.USER_ANONYMOUS;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("user {} executes {} as admin", username, action.getClass().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
Subject adminSubject = createAdminSubject();
|
|
||||||
|
|
||||||
// do not use runas, because we want only execute this action in this
|
|
||||||
// thread as administrator. Runas could affect other threads
|
|
||||||
|
|
||||||
ThreadContext.bind(adminSubject);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
action.run();
|
action.run();
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
logger.debug("release administration context for user {}/{}", principal,
|
|
||||||
subject.getPrincipal());
|
|
||||||
ThreadContext.bind(subject);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
@Override
|
||||||
|
public void runAsAdmin(Class<? extends PrivilegedAction> actionClass) {
|
||||||
|
PrivilegedAction action = injector.getInstance(actionClass);
|
||||||
|
runAsAdmin(action);
|
||||||
|
}
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private final Injector injector;
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private final org.apache.shiro.mgt.SecurityManager securityManager;
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private PrincipalCollection principalCollection;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.subject.PrincipalCollection;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.metrics.Metrics;
|
||||||
|
import sonia.scm.web.security.DefaultAdministrationContext;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.function.IntSupplier;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class DefaultCentralWorkQueue implements CentralWorkQueue, Closeable {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DefaultCentralWorkQueue.class);
|
||||||
|
|
||||||
|
private final List<UnitOfWork> queue = new ArrayList<>();
|
||||||
|
private final List<Resource> lockedResources = new ArrayList<>();
|
||||||
|
private final AtomicInteger size = new AtomicInteger();
|
||||||
|
private final AtomicLong order = new AtomicLong();
|
||||||
|
|
||||||
|
private final Injector injector;
|
||||||
|
private final Persistence persistence;
|
||||||
|
private final ExecutorService executor;
|
||||||
|
private final MeterRegistry meterRegistry;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public DefaultCentralWorkQueue(Injector injector, Persistence persistence, MeterRegistry meterRegistry) {
|
||||||
|
this(injector, persistence, meterRegistry, new ThreadCountProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
DefaultCentralWorkQueue(Injector injector, Persistence persistence, MeterRegistry meterRegistry, IntSupplier threadCountProvider) {
|
||||||
|
this.injector = injector;
|
||||||
|
this.persistence = persistence;
|
||||||
|
this.executor = createExecutorService(meterRegistry, threadCountProvider.getAsInt());
|
||||||
|
this.meterRegistry = meterRegistry;
|
||||||
|
|
||||||
|
loadFromDisk();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ExecutorService createExecutorService(MeterRegistry registry, int threadCount) {
|
||||||
|
ExecutorService executorService = Executors.newFixedThreadPool(
|
||||||
|
threadCount,
|
||||||
|
new ThreadFactoryBuilder()
|
||||||
|
.setNameFormat("CentralWorkQueue-%d")
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
Metrics.executor(registry, executorService, "CentralWorkQueue", "fixed");
|
||||||
|
return executorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enqueue append() {
|
||||||
|
return new DefaultEnqueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize() {
|
||||||
|
return size.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
executor.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFromDisk() {
|
||||||
|
for (UnitOfWork unitOfWork : persistence.loadAll()) {
|
||||||
|
unitOfWork.restore(order.incrementAndGet());
|
||||||
|
append(unitOfWork);
|
||||||
|
}
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void append(UnitOfWork unitOfWork) {
|
||||||
|
persistence.store(unitOfWork);
|
||||||
|
int queueSize = size.incrementAndGet();
|
||||||
|
queue.add(unitOfWork);
|
||||||
|
LOG.debug("add task {} to queue, queue size is now {}", unitOfWork, queueSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void run() {
|
||||||
|
Iterator<UnitOfWork> iterator = queue.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
UnitOfWork unitOfWork = iterator.next();
|
||||||
|
if (isRunnable(unitOfWork)) {
|
||||||
|
run(unitOfWork);
|
||||||
|
iterator.remove();
|
||||||
|
} else {
|
||||||
|
unitOfWork.blocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void run(UnitOfWork unitOfWork) {
|
||||||
|
lockedResources.addAll(unitOfWork.getLocks());
|
||||||
|
unitOfWork.init(injector, this::finalizeWork, meterRegistry);
|
||||||
|
LOG.trace("pass task {} to executor", unitOfWork);
|
||||||
|
executor.execute(unitOfWork);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void finalizeWork(UnitOfWork unitOfWork) {
|
||||||
|
for (Resource lock : unitOfWork.getLocks()) {
|
||||||
|
lockedResources.remove(lock);
|
||||||
|
}
|
||||||
|
persistence.remove(unitOfWork);
|
||||||
|
|
||||||
|
int queueSize = size.decrementAndGet();
|
||||||
|
LOG.debug("finish task, queue size is now {}", queueSize);
|
||||||
|
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRunnable(UnitOfWork unitOfWork) {
|
||||||
|
for (Resource resource : unitOfWork.getLocks()) {
|
||||||
|
for (Resource lock : lockedResources) {
|
||||||
|
if (resource.isBlockedBy(lock)) {
|
||||||
|
LOG.trace("skip {}, because resource {} is locked by {}", unitOfWork, resource, lock);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DefaultEnqueue implements Enqueue {
|
||||||
|
|
||||||
|
private final Set<Resource> locks = new HashSet<>();
|
||||||
|
private boolean runAsAdmin = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enqueue locks(String resourceType) {
|
||||||
|
locks.add(new Resource(resourceType));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enqueue locks(String resource, @Nullable String id) {
|
||||||
|
locks.add(new Resource(resource, id));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enqueue runAsAdmin() {
|
||||||
|
this.runAsAdmin = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enqueue(Task task) {
|
||||||
|
appendAndRun(new SimpleUnitOfWork(order.incrementAndGet(), principal(), locks, task));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enqueue(Class<? extends Runnable> task) {
|
||||||
|
appendAndRun(new InjectingUnitOfWork(order.incrementAndGet(), principal(), locks, task));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PrincipalCollection principal() {
|
||||||
|
if (runAsAdmin) {
|
||||||
|
return DefaultAdministrationContext.createAdminPrincipal();
|
||||||
|
}
|
||||||
|
return SecurityUtils.getSubject().getPrincipals();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void appendAndRun(UnitOfWork unitOfWork) {
|
||||||
|
append(unitOfWork);
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.work;
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface IndexQueueTask<T> {
|
interface Finalizer {
|
||||||
|
|
||||||
void updateIndex(Index<T> index);
|
void finalizeWork(UnitOfWork unitOfWork);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.apache.shiro.subject.PrincipalCollection;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
class InjectingUnitOfWork extends UnitOfWork {
|
||||||
|
|
||||||
|
private final Class<? extends Runnable> task;
|
||||||
|
|
||||||
|
InjectingUnitOfWork(long order, PrincipalCollection principal, Set<Resource> locks, Class<? extends Runnable> task) {
|
||||||
|
super(order, principal, locks);
|
||||||
|
this.task = task;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Runnable task(Injector injector) {
|
||||||
|
return injector.getInstance(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
scm-webapp/src/main/java/sonia/scm/work/Persistence.java
Normal file
108
scm-webapp/src/main/java/sonia/scm/work/Persistence.java
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import org.apache.commons.io.input.ClassLoaderObjectInputStream;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
import sonia.scm.store.Blob;
|
||||||
|
import sonia.scm.store.BlobStore;
|
||||||
|
import sonia.scm.store.BlobStoreFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
class Persistence {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Persistence.class);
|
||||||
|
private static final String STORE_NAME = "central-work-queue";
|
||||||
|
|
||||||
|
private final ClassLoader classLoader;
|
||||||
|
private final BlobStore store;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public Persistence(PluginLoader pluginLoader, BlobStoreFactory storeFactory) {
|
||||||
|
this(pluginLoader.getUberClassLoader(), storeFactory.withName(STORE_NAME).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
Persistence(ClassLoader classLoader, BlobStore store) {
|
||||||
|
this.classLoader = classLoader;
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<UnitOfWork> loadAll() {
|
||||||
|
List<UnitOfWork> chunks = new ArrayList<>();
|
||||||
|
for (Blob blob : store.getAll()) {
|
||||||
|
load(blob).ifPresent(chunkOfWork -> {
|
||||||
|
chunkOfWork.assignStorageId(null);
|
||||||
|
chunks.add(chunkOfWork);
|
||||||
|
});
|
||||||
|
store.remove(blob);
|
||||||
|
}
|
||||||
|
Collections.sort(chunks);
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<UnitOfWork> load(Blob blob) {
|
||||||
|
try (ObjectInputStream stream = new ClassLoaderObjectInputStream(classLoader, blob.getInputStream())) {
|
||||||
|
Object o = stream.readObject();
|
||||||
|
if (o instanceof UnitOfWork) {
|
||||||
|
return Optional.of((UnitOfWork) o);
|
||||||
|
} else {
|
||||||
|
LOG.error("loaded object is not a instance of {}: {}", UnitOfWork.class, o);
|
||||||
|
}
|
||||||
|
} catch (IOException | ClassNotFoundException ex) {
|
||||||
|
LOG.error("failed to load task from store", ex);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void store(UnitOfWork unitOfWork) {
|
||||||
|
Blob blob = store.create();
|
||||||
|
try (ObjectOutputStream outputStream = new ObjectOutputStream(blob.getOutputStream())) {
|
||||||
|
outputStream.writeObject(unitOfWork);
|
||||||
|
blob.commit();
|
||||||
|
|
||||||
|
unitOfWork.assignStorageId(blob.getId());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new NonPersistableTaskException("Failed to persist task", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(UnitOfWork unitOfWork) {
|
||||||
|
unitOfWork.getStorageId().ifPresent(store::remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
66
scm-webapp/src/main/java/sonia/scm/work/Resource.java
Normal file
66
scm-webapp/src/main/java/sonia/scm/work/Resource.java
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@EqualsAndHashCode
|
||||||
|
final class Resource implements Serializable {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
@Nullable
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
Resource(String name) {
|
||||||
|
this.name = name;
|
||||||
|
this.id = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource(String name, @Nullable String id) {
|
||||||
|
this.name = name;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isBlockedBy(Resource resource) {
|
||||||
|
if (name.equals(resource.name)) {
|
||||||
|
if (id != null && resource.id != null) {
|
||||||
|
return id.equals(resource.id);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (id != null) {
|
||||||
|
return name + ":" + id;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.apache.shiro.subject.PrincipalCollection;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
class SimpleUnitOfWork extends UnitOfWork {
|
||||||
|
|
||||||
|
private final Task task;
|
||||||
|
|
||||||
|
SimpleUnitOfWork(long order, PrincipalCollection principal, Set<Resource> locks, Task task) {
|
||||||
|
super(order, principal, locks);
|
||||||
|
this.task = task;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task task(Injector injector) {
|
||||||
|
injector.injectMembers(task);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.function.IntSupplier;
|
||||||
|
|
||||||
|
public class ThreadCountProvider implements IntSupplier {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ThreadCountProvider.class);
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String PROPERTY = "scm.central-work-queue.workers";
|
||||||
|
|
||||||
|
private final IntSupplier cpuCountProvider;
|
||||||
|
|
||||||
|
public ThreadCountProvider() {
|
||||||
|
this(() -> Runtime.getRuntime().availableProcessors());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
ThreadCountProvider(IntSupplier cpuCountProvider) {
|
||||||
|
this.cpuCountProvider = cpuCountProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAsInt() {
|
||||||
|
Integer systemProperty = Integer.getInteger(PROPERTY);
|
||||||
|
if (systemProperty == null) {
|
||||||
|
LOG.debug("derive worker count from cpu count");
|
||||||
|
return deriveFromCPUCount();
|
||||||
|
}
|
||||||
|
if (isInvalid(systemProperty)) {
|
||||||
|
LOG.warn(
|
||||||
|
"system property {} contains a invalid value {}, fall back and derive worker count from cpu count",
|
||||||
|
PROPERTY, systemProperty
|
||||||
|
);
|
||||||
|
return deriveFromCPUCount();
|
||||||
|
}
|
||||||
|
return systemProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInvalid(int value) {
|
||||||
|
return value <= 0 || value > 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int deriveFromCPUCount() {
|
||||||
|
int cpus = cpuCountProvider.getAsInt();
|
||||||
|
if (cpus > 1) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
149
scm-webapp/src/main/java/sonia/scm/work/UnitOfWork.java
Normal file
149
scm-webapp/src/main/java/sonia/scm/work/UnitOfWork.java
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.Stopwatch;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
|
import io.micrometer.core.instrument.Timer;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.apache.shiro.subject.PrincipalCollection;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.security.Impersonator;
|
||||||
|
import sonia.scm.security.Impersonator.Session;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@EqualsAndHashCode
|
||||||
|
abstract class UnitOfWork implements Runnable, Serializable, Comparable<UnitOfWork> {
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String METRIC_EXECUTION = "cwq.task.execution.duration";
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String METRIC_WAIT = "cwq.task.wait.duration";
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(UnitOfWork.class);
|
||||||
|
|
||||||
|
private long order;
|
||||||
|
private int blockCount = 0;
|
||||||
|
private int restoreCount = 0;
|
||||||
|
private final Set<Resource> locks;
|
||||||
|
private final PrincipalCollection principal;
|
||||||
|
|
||||||
|
private transient Finalizer finalizer;
|
||||||
|
private transient Runnable task;
|
||||||
|
private transient MeterRegistry meterRegistry;
|
||||||
|
private transient Impersonator impersonator;
|
||||||
|
|
||||||
|
private transient long createdAt;
|
||||||
|
private transient String storageId;
|
||||||
|
|
||||||
|
protected UnitOfWork(long order, PrincipalCollection principal, Set<Resource> locks) {
|
||||||
|
this.order = order;
|
||||||
|
this.principal = principal;
|
||||||
|
this.locks = locks;
|
||||||
|
this.createdAt = System.nanoTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOrder() {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void restore(long newOrderId) {
|
||||||
|
this.order = newOrderId;
|
||||||
|
this.createdAt = System.nanoTime();
|
||||||
|
this.restoreCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRestoreCount() {
|
||||||
|
return restoreCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void blocked() {
|
||||||
|
blockCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assignStorageId(String storageId) {
|
||||||
|
this.storageId = storageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getStorageId() {
|
||||||
|
return Optional.ofNullable(storageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Resource> getLocks() {
|
||||||
|
return locks;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init(Injector injector, Finalizer finalizer, MeterRegistry meterRegistry) {
|
||||||
|
this.task = task(injector);
|
||||||
|
this.finalizer = finalizer;
|
||||||
|
this.meterRegistry = meterRegistry;
|
||||||
|
this.impersonator = injector.getInstance(Impersonator.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Runnable task(Injector injector);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Stopwatch sw = Stopwatch.createStarted();
|
||||||
|
Timer.Sample sample = Timer.start(meterRegistry);
|
||||||
|
try (Session session = impersonator.impersonate(principal)) {
|
||||||
|
task.run();
|
||||||
|
LOG.debug("task {} finished successful after {}", task, sw.stop());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOG.error("task {} failed after {}", task, sw.stop(), ex);
|
||||||
|
} finally {
|
||||||
|
sample.stop(createExecutionTimer());
|
||||||
|
createWaitTimer().record(System.nanoTime() - createdAt, TimeUnit.NANOSECONDS);
|
||||||
|
finalizer.finalizeWork(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Timer createExecutionTimer() {
|
||||||
|
return Timer.builder(METRIC_EXECUTION)
|
||||||
|
.description("Central work queue task execution duration")
|
||||||
|
.tags("task", task.getClass().getName())
|
||||||
|
.register(meterRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Timer createWaitTimer() {
|
||||||
|
return Timer.builder(METRIC_WAIT)
|
||||||
|
.description("Central work queue task wait duration")
|
||||||
|
.tags("task", task.getClass().getName(), "restores", String.valueOf(restoreCount), "blocked", String.valueOf(blockCount))
|
||||||
|
.register(meterRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(UnitOfWork o) {
|
||||||
|
return Long.compare(order, o.order);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,20 +24,23 @@
|
|||||||
|
|
||||||
package sonia.scm.group;
|
package sonia.scm.group;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.HandlerEventType;
|
import sonia.scm.HandlerEventType;
|
||||||
import sonia.scm.search.Id;
|
import sonia.scm.search.Id;
|
||||||
import sonia.scm.search.Index;
|
import sonia.scm.search.Index;
|
||||||
|
import sonia.scm.search.IndexLogStore;
|
||||||
import sonia.scm.search.SearchEngine;
|
import sonia.scm.search.SearchEngine;
|
||||||
|
import sonia.scm.search.SerializableIndexTask;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static java.util.Collections.singletonList;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -54,6 +57,19 @@ class GroupIndexerTest {
|
|||||||
@InjectMocks
|
@InjectMocks
|
||||||
private GroupIndexer indexer;
|
private GroupIndexer indexer;
|
||||||
|
|
||||||
|
|
||||||
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
|
private Index<Group> index;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IndexLogStore indexLogStore;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<SerializableIndexTask<Group>> captor;
|
||||||
|
|
||||||
|
private final Group astronauts = new Group("xml", "astronauts");
|
||||||
|
private final Group planetCreators = new Group("xml", "planet-creators");
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnClass() {
|
void shouldReturnClass() {
|
||||||
assertThat(indexer.getType()).isEqualTo(Group.class);
|
assertThat(indexer.getType()).isEqualTo(Group.class);
|
||||||
@@ -64,58 +80,47 @@ class GroupIndexerTest {
|
|||||||
assertThat(indexer.getVersion()).isEqualTo(GroupIndexer.VERSION);
|
assertThat(indexer.getVersion()).isEqualTo(GroupIndexer.VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
|
||||||
class UpdaterTests {
|
|
||||||
|
|
||||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
@Test
|
||||||
private Index<Group> index;
|
void shouldReturnReIndexAllClass() {
|
||||||
|
assertThat(indexer.getReIndexAllTask()).isEqualTo(GroupIndexer.ReIndexAll.class);
|
||||||
|
}
|
||||||
|
|
||||||
private final Group group = new Group("xml", "astronauts");
|
@Test
|
||||||
|
void shouldCreateGroup() {
|
||||||
|
indexer.createStoreTask(astronauts).update(index);
|
||||||
|
|
||||||
@BeforeEach
|
verify(index).store(Id.of(astronauts), GroupPermissions.read(astronauts).asShiroString(), astronauts);
|
||||||
void open() {
|
}
|
||||||
when(searchEngine.forType(Group.class).getOrCreate()).thenReturn(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldStore() {
|
void shouldDeleteGroup() {
|
||||||
indexer.open().store(group);
|
indexer.createDeleteTask(astronauts).update(index);
|
||||||
|
|
||||||
verify(index).store(Id.of(group), "group:read:astronauts", group);
|
verify(index.delete()).byId(Id.of(astronauts));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDeleteById() {
|
void shouldReIndexAll() {
|
||||||
indexer.open().delete(group);
|
when(groupManager.getAll()).thenReturn(Arrays.asList(astronauts, planetCreators));
|
||||||
|
|
||||||
verify(index.delete().byType()).byId(Id.of(group));
|
GroupIndexer.ReIndexAll reIndexAll = new GroupIndexer.ReIndexAll(indexLogStore, groupManager);
|
||||||
}
|
reIndexAll.update(index);
|
||||||
|
|
||||||
@Test
|
verify(index.delete()).all();
|
||||||
void shouldReIndexAll() {
|
verify(index).store(Id.of(astronauts), GroupPermissions.read(astronauts).asShiroString(), astronauts);
|
||||||
when(groupManager.getAll()).thenReturn(singletonList(group));
|
verify(index).store(Id.of(planetCreators), GroupPermissions.read(planetCreators).asShiroString(), planetCreators);
|
||||||
|
}
|
||||||
|
|
||||||
indexer.open().reIndexAll();
|
@Test
|
||||||
|
void shouldHandleEvents() {
|
||||||
|
GroupEvent event = new GroupEvent(HandlerEventType.DELETE, astronauts);
|
||||||
|
|
||||||
verify(index.delete().byType()).all();
|
indexer.handleEvent(event);
|
||||||
verify(index).store(Id.of(group), "group:read:astronauts", group);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
verify(searchEngine.forType(Group.class)).update(captor.capture());
|
||||||
void shouldHandleEvent() {
|
captor.getValue().update(index);
|
||||||
GroupEvent event = new GroupEvent(HandlerEventType.DELETE, group);
|
verify(index.delete()).byId(Id.of(astronauts));
|
||||||
|
|
||||||
indexer.handleEvent(event);
|
|
||||||
|
|
||||||
verify(index.delete().byType()).byId(Id.of(group));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCloseIndex() {
|
|
||||||
indexer.open().close();
|
|
||||||
|
|
||||||
verify(index).close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,20 +24,25 @@
|
|||||||
|
|
||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.HandlerEventType;
|
import sonia.scm.HandlerEventType;
|
||||||
import sonia.scm.search.Id;
|
import sonia.scm.search.Id;
|
||||||
import sonia.scm.search.Index;
|
import sonia.scm.search.Index;
|
||||||
|
import sonia.scm.search.IndexLogStore;
|
||||||
import sonia.scm.search.SearchEngine;
|
import sonia.scm.search.SearchEngine;
|
||||||
|
import sonia.scm.search.SerializableIndexTask;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static java.util.Collections.singletonList;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -54,6 +59,15 @@ class RepositoryIndexerTest {
|
|||||||
@InjectMocks
|
@InjectMocks
|
||||||
private RepositoryIndexer indexer;
|
private RepositoryIndexer indexer;
|
||||||
|
|
||||||
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
|
private Index<Repository> index;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IndexLogStore indexLogStore;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<SerializableIndexTask<Repository>> captor;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnRepositoryClass() {
|
void shouldReturnRepositoryClass() {
|
||||||
assertThat(indexer.getType()).isEqualTo(Repository.class);
|
assertThat(indexer.getType()).isEqualTo(Repository.class);
|
||||||
@@ -64,61 +78,65 @@ class RepositoryIndexerTest {
|
|||||||
assertThat(indexer.getVersion()).isEqualTo(RepositoryIndexer.VERSION);
|
assertThat(indexer.getVersion()).isEqualTo(RepositoryIndexer.VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Test
|
||||||
class UpdaterTests {
|
void shouldReturnReIndexAllClass() {
|
||||||
|
assertThat(indexer.getReIndexAllTask()).isEqualTo(RepositoryIndexer.ReIndexAll.class);
|
||||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
|
||||||
private Index<Repository> index;
|
|
||||||
|
|
||||||
private Repository repository;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void open() {
|
|
||||||
when(searchEngine.forType(Repository.class).getOrCreate()).thenReturn(index);
|
|
||||||
repository = new Repository();
|
|
||||||
repository.setId("42");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldStoreRepository() {
|
|
||||||
indexer.open().store(repository);
|
|
||||||
|
|
||||||
verify(index).store(Id.of(repository), "repository:read:42", repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDeleteByRepository() {
|
|
||||||
indexer.open().delete(repository);
|
|
||||||
|
|
||||||
verify(index.delete().allTypes()).byRepository("42");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReIndexAll() {
|
|
||||||
when(repositoryManager.getAll()).thenReturn(singletonList(repository));
|
|
||||||
|
|
||||||
indexer.open().reIndexAll();
|
|
||||||
|
|
||||||
verify(index.delete().allTypes()).byTypeName(Repository.class.getName());
|
|
||||||
verify(index.delete().byType()).all();
|
|
||||||
|
|
||||||
verify(index).store(Id.of(repository), "repository:read:42", repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandleEvent() {
|
|
||||||
RepositoryEvent event = new RepositoryEvent(HandlerEventType.DELETE, repository);
|
|
||||||
|
|
||||||
indexer.handleEvent(event);
|
|
||||||
|
|
||||||
verify(index.delete().allTypes()).byRepository("42");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCloseIndex() {
|
|
||||||
indexer.open().close();
|
|
||||||
|
|
||||||
verify(index).close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateRepository() {
|
||||||
|
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||||
|
|
||||||
|
indexer.createStoreTask(heartOfGold).update(index);
|
||||||
|
|
||||||
|
verify(index).store(Id.of(heartOfGold), RepositoryPermissions.read(heartOfGold).asShiroString(), heartOfGold);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeleteRepository() {
|
||||||
|
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||||
|
|
||||||
|
indexer.createDeleteTask(heartOfGold).update(index);
|
||||||
|
|
||||||
|
verify(index.delete()).byRepository(heartOfGold);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReIndexAll() {
|
||||||
|
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||||
|
Repository puzzle = RepositoryTestData.create42Puzzle();
|
||||||
|
when(repositoryManager.getAll()).thenReturn(Arrays.asList(heartOfGold, puzzle));
|
||||||
|
|
||||||
|
RepositoryIndexer.ReIndexAll reIndexAll = new RepositoryIndexer.ReIndexAll(indexLogStore, repositoryManager);
|
||||||
|
reIndexAll.update(index);
|
||||||
|
|
||||||
|
verify(index.delete()).all();
|
||||||
|
verify(index).store(Id.of(heartOfGold), RepositoryPermissions.read(heartOfGold).asShiroString(), heartOfGold);
|
||||||
|
verify(index).store(Id.of(puzzle), RepositoryPermissions.read(puzzle).asShiroString(), puzzle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleDeleteEvents() {
|
||||||
|
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||||
|
RepositoryEvent event = new RepositoryEvent(HandlerEventType.DELETE, heartOfGold);
|
||||||
|
|
||||||
|
indexer.handleEvent(event);
|
||||||
|
|
||||||
|
verify(searchEngine.forIndices().forResource(heartOfGold)).batch(captor.capture());
|
||||||
|
captor.getValue().update(index);
|
||||||
|
verify(index.delete()).byRepository(heartOfGold);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleUpdateEvents() {
|
||||||
|
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||||
|
RepositoryEvent event = new RepositoryEvent(HandlerEventType.CREATE, heartOfGold);
|
||||||
|
|
||||||
|
indexer.handleEvent(event);
|
||||||
|
|
||||||
|
verify(searchEngine.forType(Repository.class)).update(captor.capture());
|
||||||
|
captor.getValue().update(index);
|
||||||
|
verify(index).store(Id.of(heartOfGold), RepositoryPermissions.read(heartOfGold).asShiroString(), heartOfGold);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,12 @@
|
|||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.EnumSource;
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
|
import org.mockito.Answers;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.HandlerEventType;
|
import sonia.scm.HandlerEventType;
|
||||||
@@ -35,6 +37,7 @@ import sonia.scm.repository.Repository;
|
|||||||
import sonia.scm.repository.RepositoryEvent;
|
import sonia.scm.repository.RepositoryEvent;
|
||||||
import sonia.scm.repository.RepositoryTestData;
|
import sonia.scm.repository.RepositoryTestData;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyNoInteractions;
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -42,18 +45,23 @@ import static org.mockito.Mockito.when;
|
|||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class HandlerEventIndexSyncerTest {
|
class HandlerEventIndexSyncerTest {
|
||||||
|
|
||||||
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
|
private SearchEngine searchEngine;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private Indexer<Repository> indexer;
|
private Indexer<Repository> indexer;
|
||||||
|
|
||||||
@Mock
|
@BeforeEach
|
||||||
private Indexer.Updater<Repository> updater;
|
void setUpIndexer() {
|
||||||
|
lenient().when(indexer.getType()).thenReturn(Repository.class);
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@EnumSource(value = HandlerEventType.class, mode = EnumSource.Mode.MATCH_ANY, names = "BEFORE_.*")
|
@EnumSource(value = HandlerEventType.class, mode = EnumSource.Mode.MATCH_ANY, names = "BEFORE_.*")
|
||||||
void shouldIgnoreBeforeEvents(HandlerEventType type) {
|
void shouldIgnoreBeforeEvents(HandlerEventType type) {
|
||||||
RepositoryEvent event = new RepositoryEvent(type, RepositoryTestData.create42Puzzle());
|
RepositoryEvent event = new RepositoryEvent(type, RepositoryTestData.create42Puzzle());
|
||||||
|
|
||||||
new HandlerEventIndexSyncer<>(indexer).handleEvent(event);
|
new HandlerEventIndexSyncer<>(searchEngine, indexer).handleEvent(event);
|
||||||
|
|
||||||
verifyNoInteractions(indexer);
|
verifyNoInteractions(indexer);
|
||||||
}
|
}
|
||||||
@@ -61,28 +69,29 @@ class HandlerEventIndexSyncerTest {
|
|||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@EnumSource(value = HandlerEventType.class, mode = EnumSource.Mode.INCLUDE, names = {"CREATE", "MODIFY"})
|
@EnumSource(value = HandlerEventType.class, mode = EnumSource.Mode.INCLUDE, names = {"CREATE", "MODIFY"})
|
||||||
void shouldStore(HandlerEventType type) {
|
void shouldStore(HandlerEventType type) {
|
||||||
when(indexer.open()).thenReturn(updater);
|
SerializableIndexTask<Repository> store = index -> {};
|
||||||
|
|
||||||
Repository puzzle = RepositoryTestData.create42Puzzle();
|
Repository puzzle = RepositoryTestData.create42Puzzle();
|
||||||
|
when(indexer.createStoreTask(puzzle)).thenReturn(store);
|
||||||
|
|
||||||
RepositoryEvent event = new RepositoryEvent(type, puzzle);
|
RepositoryEvent event = new RepositoryEvent(type, puzzle);
|
||||||
|
|
||||||
new HandlerEventIndexSyncer<>(indexer).handleEvent(event);
|
new HandlerEventIndexSyncer<>(searchEngine, indexer).handleEvent(event);
|
||||||
|
|
||||||
verify(updater).store(puzzle);
|
verify(searchEngine.forType(Repository.class)).update(store);
|
||||||
verify(updater).close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDelete() {
|
void shouldDelete() {
|
||||||
when(indexer.open()).thenReturn(updater);
|
SerializableIndexTask<Repository> delete = index -> {};
|
||||||
|
|
||||||
Repository puzzle = RepositoryTestData.create42Puzzle();
|
Repository puzzle = RepositoryTestData.create42Puzzle();
|
||||||
|
when(indexer.createDeleteTask(puzzle)).thenReturn(delete);
|
||||||
|
|
||||||
RepositoryEvent event = new RepositoryEvent(HandlerEventType.DELETE, puzzle);
|
RepositoryEvent event = new RepositoryEvent(HandlerEventType.DELETE, puzzle);
|
||||||
|
new HandlerEventIndexSyncer<>(searchEngine, indexer).handleEvent(event);
|
||||||
|
|
||||||
new HandlerEventIndexSyncer<>(indexer).handleEvent(event);
|
verify(searchEngine.forType(Repository.class)).update(delete);
|
||||||
|
|
||||||
verify(updater).delete(puzzle);
|
|
||||||
verify(updater).close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||||||
import sonia.scm.group.Group;
|
import sonia.scm.group.Group;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.web.security.AdministrationContext;
|
|
||||||
import sonia.scm.web.security.PrivilegedAction;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@@ -42,47 +40,39 @@ import java.util.HashSet;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class IndexBootstrapListenerTest {
|
class IndexBootstrapListenerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
private AdministrationContext administrationContext;
|
private SearchEngine searchEngine;
|
||||||
|
|
||||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
private IndexLogStore indexLogStore;
|
private IndexLogStore indexLogStore;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReIndexWithoutLog() {
|
void shouldReIndexWithoutLog() {
|
||||||
mockAdminContext();
|
|
||||||
Indexer<Repository> indexer = indexer(Repository.class, 1);
|
Indexer<Repository> indexer = indexer(Repository.class, 1);
|
||||||
Indexer.Updater<Repository> updater = updater(indexer);
|
|
||||||
|
|
||||||
mockEmptyIndexLog(Repository.class);
|
mockEmptyIndexLog(Repository.class);
|
||||||
doInitialization(indexer);
|
doInitialization(indexer);
|
||||||
|
|
||||||
verify(updater).reIndexAll();
|
verify(searchEngine.forType(Repository.class)).update(RepositoryReIndexAllTask.class);
|
||||||
verify(updater).close();
|
|
||||||
verify(indexLogStore.defaultIndex()).log(Repository.class, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReIndexIfVersionWasUpdated() {
|
void shouldReIndexIfVersionWasUpdated() {
|
||||||
mockAdminContext();
|
|
||||||
Indexer<User> indexer = indexer(User.class, 2);
|
Indexer<User> indexer = indexer(User.class, 2);
|
||||||
Indexer.Updater<User> updater = updater(indexer);
|
|
||||||
|
|
||||||
mockIndexLog(User.class, 1);
|
mockIndexLog(User.class, 1);
|
||||||
doInitialization(indexer);
|
doInitialization(indexer);
|
||||||
|
|
||||||
verify(updater).reIndexAll();
|
verify(searchEngine.forType(User.class)).update(UserReIndexAllTask.class);
|
||||||
verify(updater).close();
|
|
||||||
verify(indexLogStore.defaultIndex()).log(User.class, 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -92,7 +82,7 @@ class IndexBootstrapListenerTest {
|
|||||||
mockIndexLog(Group.class, 3);
|
mockIndexLog(Group.class, 3);
|
||||||
doInitialization(indexer);
|
doInitialization(indexer);
|
||||||
|
|
||||||
verify(indexer, never()).open();
|
verifyNoInteractions(searchEngine);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> void mockIndexLog(Class<T> type, int version) {
|
private <T> void mockIndexLog(Class<T> type, int version) {
|
||||||
@@ -107,13 +97,6 @@ class IndexBootstrapListenerTest {
|
|||||||
when(indexLogStore.defaultIndex().get(type)).thenReturn(Optional.ofNullable(indexLog));
|
when(indexLogStore.defaultIndex().get(type)).thenReturn(Optional.ofNullable(indexLog));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mockAdminContext() {
|
|
||||||
doAnswer(ic -> {
|
|
||||||
PrivilegedAction action = ic.getArgument(0);
|
|
||||||
action.run();
|
|
||||||
return null;
|
|
||||||
}).when(administrationContext).runAsAdmin(any(PrivilegedAction.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
private void doInitialization(Indexer... indexers) {
|
private void doInitialization(Indexer... indexers) {
|
||||||
@@ -125,7 +108,7 @@ class IndexBootstrapListenerTest {
|
|||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
private IndexBootstrapListener listener(Indexer... indexers) {
|
private IndexBootstrapListener listener(Indexer... indexers) {
|
||||||
return new IndexBootstrapListener(
|
return new IndexBootstrapListener(
|
||||||
administrationContext, indexLogStore, new HashSet<>(Arrays.asList(indexers))
|
searchEngine, indexLogStore, new HashSet<>(Arrays.asList(indexers))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,15 +116,38 @@ class IndexBootstrapListenerTest {
|
|||||||
private <T> Indexer<T> indexer(Class<T> type, int version) {
|
private <T> Indexer<T> indexer(Class<T> type, int version) {
|
||||||
Indexer<T> indexer = mock(Indexer.class);
|
Indexer<T> indexer = mock(Indexer.class);
|
||||||
when(indexer.getType()).thenReturn(type);
|
when(indexer.getType()).thenReturn(type);
|
||||||
when(indexer.getVersion()).thenReturn(version);
|
lenient().when(indexer.getVersion()).thenReturn(version);
|
||||||
|
lenient().when(indexer.getReIndexAllTask()).thenAnswer(ic -> {
|
||||||
|
if (type == User.class) {
|
||||||
|
return UserReIndexAllTask.class;
|
||||||
|
}
|
||||||
|
return RepositoryReIndexAllTask.class;
|
||||||
|
});
|
||||||
return indexer;
|
return indexer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
public static class RepositoryReIndexAllTask extends Indexer.ReIndexAllTask<Repository> {
|
||||||
private <T> Indexer.Updater<T> updater(Indexer<T> indexer) {
|
|
||||||
Indexer.Updater<T> updater = mock(Indexer.Updater.class);
|
public RepositoryReIndexAllTask(IndexLogStore logStore, Class<Repository> type, int version) {
|
||||||
when(indexer.open()).thenReturn(updater);
|
super(logStore, type, version);
|
||||||
return updater;
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(Index<Repository> index) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UserReIndexAllTask extends Indexer.ReIndexAllTask<User> {
|
||||||
|
|
||||||
|
public UserReIndexAllTask(IndexLogStore logStore, Class<User> type, int version) {
|
||||||
|
super(logStore, type, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(Index<User> index) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
188
scm-webapp/src/test/java/sonia/scm/search/IndexManagerTest.java
Normal file
188
scm-webapp/src/test/java/sonia/scm/search/IndexManagerTest.java
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import org.apache.lucene.analysis.core.SimpleAnalyzer;
|
||||||
|
import org.apache.lucene.document.Document;
|
||||||
|
import org.apache.lucene.document.Field;
|
||||||
|
import org.apache.lucene.document.TextField;
|
||||||
|
import org.apache.lucene.index.IndexReader;
|
||||||
|
import org.apache.lucene.index.IndexWriter;
|
||||||
|
import org.assertj.core.api.AssertionsForClassTypes;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class IndexManagerTest {
|
||||||
|
|
||||||
|
private Path directory;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AnalyzerFactory analyzerFactory;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LuceneSearchableType searchableType;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SCMContextProvider context;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PluginLoader pluginLoader;
|
||||||
|
|
||||||
|
private IndexManager indexManager;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createIndexWriterFactory(@TempDir Path tempDirectory) {
|
||||||
|
this.directory = tempDirectory;
|
||||||
|
when(context.resolve(Paths.get("index"))).thenReturn(tempDirectory.resolve("index"));
|
||||||
|
when(analyzerFactory.create(any(LuceneSearchableType.class), any(IndexOptions.class))).thenReturn(new SimpleAnalyzer());
|
||||||
|
when(pluginLoader.getUberClassLoader()).thenReturn(IndexManagerTest.class.getClassLoader());
|
||||||
|
indexManager = new IndexManager(context, pluginLoader, analyzerFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateNewIndex() throws IOException {
|
||||||
|
try (IndexWriter writer = open(Songs.class, "new-index")) {
|
||||||
|
addDoc(writer, "Trillian");
|
||||||
|
}
|
||||||
|
assertThat(directory.resolve("index").resolve("songs").resolve("new-index")).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateNewIndexForEachType() throws IOException {
|
||||||
|
try (IndexWriter writer = open(Songs.class, "new-index")) {
|
||||||
|
addDoc(writer, "Trillian");
|
||||||
|
}
|
||||||
|
try (IndexWriter writer = open(Lyrics.class, "new-index")) {
|
||||||
|
addDoc(writer, "Trillian");
|
||||||
|
}
|
||||||
|
assertThat(directory.resolve("index").resolve("songs").resolve("new-index")).exists();
|
||||||
|
assertThat(directory.resolve("index").resolve("lyrics").resolve("new-index")).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnAllCreatedIndices() throws IOException {
|
||||||
|
try (IndexWriter writer = open(Songs.class, "special")) {
|
||||||
|
addDoc(writer, "Trillian");
|
||||||
|
}
|
||||||
|
try (IndexWriter writer = open(Lyrics.class, "awesome")) {
|
||||||
|
addDoc(writer, "Trillian");
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(indexManager.all())
|
||||||
|
.anySatisfy(details -> {
|
||||||
|
assertThat(details.getType()).isEqualTo(Songs.class);
|
||||||
|
assertThat(details.getName()).isEqualTo("special");
|
||||||
|
})
|
||||||
|
.anySatisfy(details -> {
|
||||||
|
assertThat(details.getType()).isEqualTo(Lyrics.class);
|
||||||
|
assertThat(details.getName()).isEqualTo("awesome");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRestoreIndicesOnCreation() throws IOException {
|
||||||
|
try (IndexWriter writer = open(Songs.class, "special")) {
|
||||||
|
addDoc(writer, "Trillian");
|
||||||
|
}
|
||||||
|
try (IndexWriter writer = open(Lyrics.class, "awesome")) {
|
||||||
|
addDoc(writer, "Trillian");
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(new IndexManager(context, pluginLoader, analyzerFactory).all())
|
||||||
|
.anySatisfy(details -> {
|
||||||
|
AssertionsForClassTypes.assertThat(details.getType()).isEqualTo(Songs.class);
|
||||||
|
AssertionsForClassTypes.assertThat(details.getName()).isEqualTo("special");
|
||||||
|
})
|
||||||
|
.anySatisfy(details -> {
|
||||||
|
AssertionsForClassTypes.assertThat(details.getType()).isEqualTo(Lyrics.class);
|
||||||
|
AssertionsForClassTypes.assertThat(details.getName()).isEqualTo("awesome");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
private IndexWriter open(Class type, String indexName) throws IOException {
|
||||||
|
lenient().when(searchableType.getType()).thenReturn(type);
|
||||||
|
when(searchableType.getName()).thenReturn(type.getSimpleName().toLowerCase(Locale.ENGLISH));
|
||||||
|
return indexManager.openForWrite(new IndexParams(indexName, searchableType, IndexOptions.defaults()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldOpenExistingIndex() throws IOException {
|
||||||
|
try (IndexWriter writer = open(Songs.class, "reused")) {
|
||||||
|
addDoc(writer, "Dent");
|
||||||
|
}
|
||||||
|
try (IndexWriter writer = open(Songs.class, "reused")) {
|
||||||
|
assertThat(writer.getFieldNames()).contains("hitchhiker");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseAnalyzerFromFactory() throws IOException {
|
||||||
|
try (IndexWriter writer = open(Songs.class, "new-index")) {
|
||||||
|
assertThat(writer.getAnalyzer()).isInstanceOf(SimpleAnalyzer.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldOpenIndexForRead() throws IOException {
|
||||||
|
try (IndexWriter writer = open(Songs.class, "idx-for-read")) {
|
||||||
|
addDoc(writer, "Dent");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (IndexReader reader = indexManager.openForRead(searchableType, "idx-for-read")) {
|
||||||
|
assertThat(reader.numDocs()).isOne();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDoc(IndexWriter writer, String name) throws IOException {
|
||||||
|
Document doc = new Document();
|
||||||
|
doc.add(new TextField("hitchhiker", name, Field.Store.YES));
|
||||||
|
writer.addDocument(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Songs {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Lyrics {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sonia.scm.search;
|
|
||||||
|
|
||||||
import org.apache.lucene.analysis.core.SimpleAnalyzer;
|
|
||||||
import org.apache.lucene.document.Document;
|
|
||||||
import org.apache.lucene.document.Field;
|
|
||||||
import org.apache.lucene.document.TextField;
|
|
||||||
import org.apache.lucene.index.IndexReader;
|
|
||||||
import org.apache.lucene.index.IndexWriter;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import sonia.scm.SCMContextProvider;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class IndexOpenerTest {
|
|
||||||
|
|
||||||
private Path directory;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private AnalyzerFactory analyzerFactory;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private LuceneSearchableType searchableType;
|
|
||||||
|
|
||||||
private IndexOpener indexOpener;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void createIndexWriterFactory(@TempDir Path tempDirectory) {
|
|
||||||
this.directory = tempDirectory;
|
|
||||||
SCMContextProvider context = mock(SCMContextProvider.class);
|
|
||||||
when(context.resolve(Paths.get("index"))).thenReturn(tempDirectory);
|
|
||||||
when(analyzerFactory.create(any(LuceneSearchableType.class), any(IndexOptions.class))).thenReturn(new SimpleAnalyzer());
|
|
||||||
indexOpener = new IndexOpener(context, analyzerFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCreateNewIndex() throws IOException {
|
|
||||||
try (IndexWriter writer = open("new-index")) {
|
|
||||||
addDoc(writer, "Trillian");
|
|
||||||
}
|
|
||||||
assertThat(directory.resolve("new-index")).exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IndexWriter open(String index) throws IOException {
|
|
||||||
return indexOpener.openForWrite(new IndexParams(index, searchableType, IndexOptions.defaults()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldOpenExistingIndex() throws IOException {
|
|
||||||
try (IndexWriter writer = open("reused")) {
|
|
||||||
addDoc(writer, "Dent");
|
|
||||||
}
|
|
||||||
try (IndexWriter writer = open("reused")) {
|
|
||||||
assertThat(writer.getFieldNames()).contains("hitchhiker");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldUseAnalyzerFromFactory() throws IOException {
|
|
||||||
try (IndexWriter writer = open("new-index")) {
|
|
||||||
assertThat(writer.getAnalyzer()).isInstanceOf(SimpleAnalyzer.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldOpenIndexForRead() throws IOException {
|
|
||||||
try (IndexWriter writer = open("idx-for-read")) {
|
|
||||||
addDoc(writer, "Dent");
|
|
||||||
}
|
|
||||||
|
|
||||||
try (IndexReader reader = indexOpener.openForRead("idx-for-read")) {
|
|
||||||
assertThat(reader.numDocs()).isOne();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addDoc(IndexWriter writer, String name) throws IOException {
|
|
||||||
Document doc = new Document();
|
|
||||||
doc.add(new TextField("hitchhiker", name, Field.Store.YES));
|
|
||||||
writer.addDocument(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sonia.scm.search;
|
|
||||||
|
|
||||||
import lombok.Value;
|
|
||||||
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
|
||||||
import org.apache.lucene.index.DirectoryReader;
|
|
||||||
import org.apache.lucene.index.IndexWriter;
|
|
||||||
import org.apache.lucene.index.IndexWriterConfig;
|
|
||||||
import org.apache.lucene.store.ByteBuffersDirectory;
|
|
||||||
import org.apache.lucene.store.Directory;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.assertj.core.api.Assertions.in;
|
|
||||||
import static org.awaitility.Awaitility.await;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class IndexQueueTest {
|
|
||||||
|
|
||||||
private Directory directory;
|
|
||||||
|
|
||||||
private IndexQueue queue;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void createQueue() throws IOException {
|
|
||||||
directory = new ByteBuffersDirectory();
|
|
||||||
IndexOpener opener = mock(IndexOpener.class);
|
|
||||||
when(opener.openForWrite(any(IndexParams.class))).thenAnswer(ic -> {
|
|
||||||
IndexWriterConfig config = new IndexWriterConfig(new StandardAnalyzer());
|
|
||||||
config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
|
|
||||||
return new IndexWriter(directory, config);
|
|
||||||
});
|
|
||||||
|
|
||||||
LuceneIndexFactory indexFactory = new LuceneIndexFactory(opener);
|
|
||||||
queue = new IndexQueue(indexFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
void closeQueue() throws IOException {
|
|
||||||
queue.close();
|
|
||||||
directory.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldWriteToIndex() throws Exception {
|
|
||||||
try (Index<Account> index = getIndex(Account.class)) {
|
|
||||||
index.store(Id.of("tricia"), null, new Account("tricia", "Trillian", "McMillan"));
|
|
||||||
index.store(Id.of("dent"), null, new Account("dent", "Arthur", "Dent"));
|
|
||||||
}
|
|
||||||
assertDocCount(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> Index<T> getIndex(Class<T> type) {
|
|
||||||
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
|
||||||
LuceneSearchableType searchableType = resolver.resolve(type);
|
|
||||||
IndexParams indexParams = new IndexParams("default", searchableType, IndexOptions.defaults());
|
|
||||||
return queue.getQueuedIndex(indexParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldWriteMultiThreaded() throws Exception {
|
|
||||||
ExecutorService executorService = Executors.newFixedThreadPool(4);
|
|
||||||
for (int i = 0; i < 20; i++) {
|
|
||||||
executorService.execute(new IndexNumberTask(i));
|
|
||||||
}
|
|
||||||
executorService.execute(() -> {
|
|
||||||
try (Index<IndexedNumber> index = getIndex(IndexedNumber.class)) {
|
|
||||||
index.delete().byType().byId(Id.of(String.valueOf(12)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
executorService.shutdown();
|
|
||||||
|
|
||||||
assertDocCount(19);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertDocCount(int expectedCount) throws IOException {
|
|
||||||
// wait until all tasks are finished
|
|
||||||
await().until(() -> queue.getSize() == 0);
|
|
||||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
|
||||||
assertThat(reader.numDocs()).isEqualTo(expectedCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Value
|
|
||||||
@IndexedType
|
|
||||||
public static class Account {
|
|
||||||
@Indexed
|
|
||||||
String username;
|
|
||||||
@Indexed
|
|
||||||
String firstName;
|
|
||||||
@Indexed
|
|
||||||
String lastName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Value
|
|
||||||
@IndexedType
|
|
||||||
public static class IndexedNumber {
|
|
||||||
@Indexed
|
|
||||||
int value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class IndexNumberTask implements Runnable {
|
|
||||||
|
|
||||||
private final int number;
|
|
||||||
|
|
||||||
public IndexNumberTask(int number) {
|
|
||||||
this.number = number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try (Index<IndexedNumber> index = getIndex(IndexedNumber.class)) {
|
|
||||||
index.store(Id.of(String.valueOf(number)), null, new IndexedNumber(number));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class LuceneIndexFactoryTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IndexManager indexManager;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private LuceneIndexFactory indexFactory;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCallOpenOnCreation() {
|
||||||
|
LuceneIndex<Repository> index = indexFactory.create(params("default", Repository.class));
|
||||||
|
assertThat(index.getWriter().getUsageCounter()).isOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCallOpenOnReturn() {
|
||||||
|
indexFactory.create(params("default", Repository.class));
|
||||||
|
indexFactory.create(params("default", Repository.class));
|
||||||
|
LuceneIndex<Repository> index = indexFactory.create(params("default", Repository.class));
|
||||||
|
assertThat(index.getWriter().getUsageCounter()).isEqualTo(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("AssertBetweenInconvertibleTypes")
|
||||||
|
void shouldReturnDifferentIndexForDifferentTypes() {
|
||||||
|
LuceneIndex<Repository> repository = indexFactory.create(params("default", Repository.class));
|
||||||
|
LuceneIndex<User> user = indexFactory.create(params("default", User.class));
|
||||||
|
|
||||||
|
assertThat(repository).isNotSameAs(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnDifferentIndexForDifferentIndexNames() {
|
||||||
|
LuceneIndex<Repository> def = indexFactory.create(params("default", Repository.class));
|
||||||
|
LuceneIndex<Repository> other = indexFactory.create(params("other", Repository.class));
|
||||||
|
|
||||||
|
assertThat(def).isNotSameAs(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnSameIndex() {
|
||||||
|
LuceneIndex<Repository> one = indexFactory.create(params("default", Repository.class));
|
||||||
|
LuceneIndex<Repository> two = indexFactory.create(params("default", Repository.class));
|
||||||
|
assertThat(one).isSameAs(two);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexParams params(String indexName, Class<?> type) {
|
||||||
|
return new IndexParams(indexName, SearchableTypes.create(type), IndexOptions.defaults());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ package sonia.scm.search;
|
|||||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||||
import org.apache.lucene.document.Document;
|
|
||||||
import org.apache.lucene.index.DirectoryReader;
|
import org.apache.lucene.index.DirectoryReader;
|
||||||
import org.apache.lucene.index.IndexWriter;
|
import org.apache.lucene.index.IndexWriter;
|
||||||
import org.apache.lucene.index.IndexWriterConfig;
|
import org.apache.lucene.index.IndexWriterConfig;
|
||||||
@@ -39,12 +38,26 @@ import org.apache.lucene.search.TopDocs;
|
|||||||
import org.apache.lucene.store.ByteBuffersDirectory;
|
import org.apache.lucene.store.ByteBuffersDirectory;
|
||||||
import org.apache.lucene.store.Directory;
|
import org.apache.lucene.store.Directory;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryTestData;
|
||||||
|
import sonia.scm.repository.RepositoryType;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static sonia.scm.search.FieldNames.*;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sonia.scm.search.FieldNames.ID;
|
||||||
|
import static sonia.scm.search.FieldNames.PERMISSION;
|
||||||
|
import static sonia.scm.search.FieldNames.REPOSITORY;
|
||||||
|
|
||||||
class LuceneIndexTest {
|
class LuceneIndexTest {
|
||||||
|
|
||||||
@@ -77,15 +90,6 @@ class LuceneIndexTest {
|
|||||||
assertHits("value", "content", 1);
|
assertHits("value", "content", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldStoreUidOfObject() throws IOException {
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
|
||||||
index.store(ONE, null, new Storable("Awesome content which should be indexed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
assertHits(UID, "one/storable", 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldStoreIdOfObject() throws IOException {
|
void shouldStoreIdOfObject() throws IOException {
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||||
@@ -104,15 +108,6 @@ class LuceneIndexTest {
|
|||||||
assertHits(REPOSITORY, "4211", 1);
|
assertHits(REPOSITORY, "4211", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldStoreTypeOfObject() throws IOException {
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
|
||||||
index.store(ONE, null, new Storable("Some other text"));
|
|
||||||
}
|
|
||||||
|
|
||||||
assertHits(TYPE, "storable", 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDeleteById() throws IOException {
|
void shouldDeleteById() throws IOException {
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||||
@@ -120,131 +115,54 @@ class LuceneIndexTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||||
index.delete().byType().byId(ONE);
|
index.delete().byId(ONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertHits(ID, "one", 0);
|
assertHits(ID, "one", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDeleteAllByType() throws IOException {
|
void shouldDeleteByIdAndRepository() throws IOException {
|
||||||
|
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||||
|
Repository puzzle42 = RepositoryTestData.createHeartOfGold();
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||||
index.store(ONE, null, new Storable("content"));
|
index.store(ONE.withRepository(heartOfGold), null, new Storable("content"));
|
||||||
index.store(Id.of("two"), null, new Storable("content"));
|
index.store(ONE.withRepository(puzzle42), null, new Storable("content"));
|
||||||
}
|
|
||||||
|
|
||||||
try (LuceneIndex<OtherStorable> index = createIndex(OtherStorable.class)) {
|
|
||||||
index.store(Id.of("three"), null, new OtherStorable("content"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||||
index.delete().byType().all();
|
index.delete().byId(ONE.withRepository(heartOfGold));
|
||||||
}
|
}
|
||||||
|
|
||||||
assertHits("value", "content", 1);
|
assertHits("value", "content", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDeleteByIdAnyType() throws IOException {
|
void shouldDeleteAll() throws IOException {
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||||
index.store(ONE, null, new Storable("Some text"));
|
index.store(ONE, null, new Storable("content"));
|
||||||
}
|
index.store(TWO, null, new Storable("content"));
|
||||||
|
|
||||||
try (LuceneIndex<OtherStorable> index = createIndex(OtherStorable.class)) {
|
|
||||||
index.store(ONE, null, new OtherStorable("Some other text"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||||
index.delete().byType().byId(ONE);
|
index.delete().all();
|
||||||
}
|
}
|
||||||
|
|
||||||
assertHits(ID, "one", 1);
|
assertHits("value", "content", 0);
|
||||||
ScoreDoc[] docs = assertHits(ID, "one", 1);
|
|
||||||
Document doc = doc(docs[0].doc);
|
|
||||||
assertThat(doc.get("value")).isEqualTo("Some other text");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDeleteByIdAndRepository() throws IOException {
|
|
||||||
Id withRepository = ONE.withRepository("4211");
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
|
||||||
index.store(ONE, null, new Storable("Some other text"));
|
|
||||||
index.store(withRepository, null, new Storable("New stuff"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
|
||||||
index.delete().byType().byId(withRepository);
|
|
||||||
}
|
|
||||||
|
|
||||||
ScoreDoc[] docs = assertHits(ID, "one", 1);
|
|
||||||
Document doc = doc(docs[0].doc);
|
|
||||||
assertThat(doc.get("value")).isEqualTo("Some other text");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDeleteByRepository() throws IOException {
|
void shouldDeleteByRepository() throws IOException {
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||||
index.store(ONE.withRepository("4211"), null, new Storable("Some other text"));
|
index.store(ONE.withRepository("4211"), null, new Storable("content"));
|
||||||
index.store(ONE.withRepository("4212"), null, new Storable("New stuff"));
|
index.store(TWO.withRepository("4212"), null, new Storable("content"));
|
||||||
}
|
}
|
||||||
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||||
index.delete().byType().byRepository("4212");
|
index.delete().byRepository("4212");
|
||||||
}
|
}
|
||||||
|
|
||||||
assertHits(ID, "one", 1);
|
assertHits("value", "content", 1);
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDeleteByRepositoryAndType() throws IOException {
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
|
||||||
index.store(ONE.withRepository("4211"), null, new Storable("some text"));
|
|
||||||
index.store(TWO.withRepository("4211"), null, new Storable("some text"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try (LuceneIndex<OtherStorable> index = createIndex(OtherStorable.class)) {
|
|
||||||
index.store(ONE.withRepository("4211"), null, new OtherStorable("some text"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
|
||||||
index.delete().byType().byRepository("4211");
|
|
||||||
}
|
|
||||||
|
|
||||||
ScoreDoc[] docs = assertHits("value", "text", 1);
|
|
||||||
Document doc = doc(docs[0].doc);
|
|
||||||
assertThat(doc.get(TYPE)).isEqualTo("otherStorable");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDeleteAllByRepository() throws IOException {
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
|
||||||
index.store(ONE.withRepository("4211"), null, new Storable("some text"));
|
|
||||||
index.store(TWO.withRepository("4211"), null, new Storable("some text"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try (LuceneIndex<OtherStorable> index = createIndex(OtherStorable.class)) {
|
|
||||||
index.store(ONE.withRepository("4211"), null, new OtherStorable("some text"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
|
||||||
index.delete().allTypes().byRepository("4211");
|
|
||||||
}
|
|
||||||
|
|
||||||
assertHits("value", "text", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDeleteAllByTypeName() throws IOException {
|
|
||||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
|
||||||
index.store(ONE, null, new Storable("some text"));
|
|
||||||
index.store(TWO, null, new Storable("some text"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try (LuceneIndex<OtherStorable> index = createIndex(OtherStorable.class)) {
|
|
||||||
index.delete().allTypes().byTypeName("storable");
|
|
||||||
}
|
|
||||||
|
|
||||||
assertHits("value", "text", 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -256,12 +174,69 @@ class LuceneIndexTest {
|
|||||||
assertHits(PERMISSION, "repo:4211:read", 1);
|
assertHits(PERMISSION, "repo:4211:read", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Document doc(int doc) throws IOException {
|
@Test
|
||||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
void shouldReturnDetails() {
|
||||||
return reader.document(doc);
|
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||||
|
IndexDetails details = index.getDetails();
|
||||||
|
assertThat(details.getType()).isEqualTo(Storable.class);
|
||||||
|
assertThat(details.getName()).isEqualTo("default");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ExceptionTests {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IndexWriter writer;
|
||||||
|
|
||||||
|
private LuceneIndex<Storable> index;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUpIndex() {
|
||||||
|
index = createIndex(Storable.class, () -> writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowSearchEngineExceptionOnStore() throws IOException {
|
||||||
|
when(writer.updateDocument(any(), any())).thenThrow(new IOException("failed to store"));
|
||||||
|
|
||||||
|
Storable storable = new Storable("Some other text");
|
||||||
|
assertThrows(SearchEngineException.class, () -> index.store(ONE, null, storable));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowSearchEngineExceptionOnDeleteById() throws IOException {
|
||||||
|
when(writer.deleteDocuments(any(Term.class))).thenThrow(new IOException("failed to delete"));
|
||||||
|
|
||||||
|
Index.Deleter deleter = index.delete();
|
||||||
|
assertThrows(SearchEngineException.class, () -> deleter.byId(ONE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowSearchEngineExceptionOnDeleteAll() throws IOException {
|
||||||
|
when(writer.deleteAll()).thenThrow(new IOException("failed to delete"));
|
||||||
|
|
||||||
|
Index.Deleter deleter = index.delete();
|
||||||
|
assertThrows(SearchEngineException.class, deleter::all);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowSearchEngineExceptionOnDeleteByRepository() throws IOException {
|
||||||
|
when(writer.deleteDocuments(any(Term.class))).thenThrow(new IOException("failed to delete"));
|
||||||
|
|
||||||
|
Index.Deleter deleter = index.delete();
|
||||||
|
assertThrows(SearchEngineException.class, () -> deleter.byRepository("42"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowSearchEngineExceptionOnClose() throws IOException {
|
||||||
|
doThrow(new IOException("failed to delete")).when(writer).close();
|
||||||
|
assertThrows(SearchEngineException.class, () -> index.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
private ScoreDoc[] assertHits(String field, String value, int expectedHits) throws IOException {
|
private ScoreDoc[] assertHits(String field, String value, int expectedHits) throws IOException {
|
||||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||||
@@ -272,15 +247,25 @@ class LuceneIndexTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> LuceneIndex<T> createIndex(Class<T> type) throws IOException {
|
private <T> LuceneIndex<T> createIndex(Class<T> type) {
|
||||||
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
return createIndex(type, this::createWriter);
|
||||||
return new LuceneIndex<>(resolver.resolve(type), createWriter());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndexWriter createWriter() throws IOException {
|
private <T> LuceneIndex<T> createIndex(Class<T> type, Supplier<IndexWriter> writerFactor) {
|
||||||
|
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
||||||
|
return new LuceneIndex<>(
|
||||||
|
new IndexParams("default", resolver.resolve(type), IndexOptions.defaults()), writerFactor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexWriter createWriter() {
|
||||||
IndexWriterConfig config = new IndexWriterConfig(new StandardAnalyzer());
|
IndexWriterConfig config = new IndexWriterConfig(new StandardAnalyzer());
|
||||||
config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
|
config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
|
||||||
return new IndexWriter(directory, config);
|
try {
|
||||||
|
return new IndexWriter(directory, config);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new SearchEngineException("failed to open index writer", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
@@ -290,11 +275,4 @@ class LuceneIndexTest {
|
|||||||
String value;
|
String value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
|
||||||
@IndexedType
|
|
||||||
private static class OtherStorable {
|
|
||||||
@Indexed
|
|
||||||
String value;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Guice;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Answers;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class LuceneInjectingIndexTaskTest {
|
||||||
|
|
||||||
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
|
private LuceneIndexFactory indexFactory;
|
||||||
|
|
||||||
|
private static String capturedValue;
|
||||||
|
|
||||||
|
private final SearchableTypeResolver resolver = new SearchableTypeResolver(User.class);
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void cleanUp() {
|
||||||
|
capturedValue = "notAsExpected";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldInjectAndUpdate() {
|
||||||
|
Injector injector = createInjector();
|
||||||
|
|
||||||
|
LuceneSearchableType searchableType = resolver.resolve(User.class);
|
||||||
|
IndexParams params = new IndexParams("default", searchableType, IndexOptions.defaults());
|
||||||
|
|
||||||
|
LuceneInjectingIndexTask task = new LuceneInjectingIndexTask(params, InjectingTask.class);
|
||||||
|
injector.injectMembers(task);
|
||||||
|
|
||||||
|
task.run();
|
||||||
|
|
||||||
|
assertThat(capturedValue).isEqualTo("runAsExpected");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Injector createInjector() {
|
||||||
|
return Guice.createInjector(new AbstractModule() {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(SearchableTypeResolver.class).toInstance(resolver);
|
||||||
|
bind(LuceneIndexFactory.class).toInstance(indexFactory);
|
||||||
|
bind(String.class).toInstance("runAsExpected");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class InjectingTask implements IndexTask<String> {
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public InjectingTask(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(Index<String> index) {
|
||||||
|
capturedValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -42,7 +42,6 @@ import org.apache.lucene.index.IndexWriter;
|
|||||||
import org.apache.lucene.index.IndexWriterConfig;
|
import org.apache.lucene.index.IndexWriterConfig;
|
||||||
import org.apache.lucene.store.ByteBuffersDirectory;
|
import org.apache.lucene.store.ByteBuffersDirectory;
|
||||||
import org.apache.lucene.store.Directory;
|
import org.apache.lucene.store.Directory;
|
||||||
import org.apache.shiro.authz.AuthorizationException;
|
|
||||||
import org.github.sdorra.jse.ShiroExtension;
|
import org.github.sdorra.jse.ShiroExtension;
|
||||||
import org.github.sdorra.jse.SubjectAware;
|
import org.github.sdorra.jse.SubjectAware;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@@ -70,7 +69,7 @@ class LuceneQueryBuilderTest {
|
|||||||
private Directory directory;
|
private Directory directory;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private IndexOpener opener;
|
private IndexManager opener;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUpDirectory() {
|
void setUpDirectory() {
|
||||||
@@ -181,17 +180,6 @@ class LuceneQueryBuilderTest {
|
|||||||
assertThat(result.getTotalHits()).isOne();
|
assertThat(result.getTotalHits()).isOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldIgnoreHitsOfOtherType() throws IOException {
|
|
||||||
try (IndexWriter writer = writer()) {
|
|
||||||
writer.addDocument(inetOrgPersonDoc("Arthur", "Dent", "Arthur Dent", "4211"));
|
|
||||||
writer.addDocument(personDoc("Dent"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult result = query(InetOrgPerson.class, "Dent");
|
|
||||||
assertThat(result.getTotalHits()).isOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldThrowQueryParseExceptionOnInvalidQuery() throws IOException {
|
void shouldThrowQueryParseExceptionOnInvalidQuery() throws IOException {
|
||||||
try (IndexWriter writer = writer()) {
|
try (IndexWriter writer = writer()) {
|
||||||
@@ -251,17 +239,6 @@ class LuceneQueryBuilderTest {
|
|||||||
assertThat(result.getHits()).hasSize(3);
|
assertThat(result.getHits()).hasSize(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnOnlyHitsOfTypeForExpertQuery() throws IOException {
|
|
||||||
try (IndexWriter writer = writer()) {
|
|
||||||
writer.addDocument(inetOrgPersonDoc("Ford", "Prefect", "Ford Prefect", "4211"));
|
|
||||||
writer.addDocument(personDoc("Prefect"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult result = query(InetOrgPerson.class, "lastName:prefect");
|
|
||||||
assertThat(result.getTotalHits()).isEqualTo(1L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnOnlyPermittedHits() throws IOException {
|
void shouldReturnOnlyPermittedHits() throws IOException {
|
||||||
try (IndexWriter writer = writer()) {
|
try (IndexWriter writer = writer()) {
|
||||||
@@ -302,10 +279,11 @@ class LuceneQueryBuilderTest {
|
|||||||
|
|
||||||
QueryResult result;
|
QueryResult result;
|
||||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||||
when(opener.openForRead("default")).thenReturn(reader);
|
|
||||||
SearchableTypeResolver resolver = new SearchableTypeResolver(Simple.class);
|
SearchableTypeResolver resolver = new SearchableTypeResolver(Simple.class);
|
||||||
|
LuceneSearchableType searchableType = resolver.resolve(Simple.class);
|
||||||
|
when(opener.openForRead(searchableType, "default")).thenReturn(reader);
|
||||||
LuceneQueryBuilder<Simple> builder = new LuceneQueryBuilder<>(
|
LuceneQueryBuilder<Simple> builder = new LuceneQueryBuilder<>(
|
||||||
opener, "default", resolver.resolve(Simple.class), new StandardAnalyzer()
|
opener, "default", searchableType, new StandardAnalyzer()
|
||||||
);
|
);
|
||||||
result = builder.repository("cde").execute("content:awesome");
|
result = builder.repository("cde").execute("content:awesome");
|
||||||
}
|
}
|
||||||
@@ -560,9 +538,9 @@ class LuceneQueryBuilderTest {
|
|||||||
|
|
||||||
private <T> long count(Class<T> type, String queryString) throws IOException {
|
private <T> long count(Class<T> type, String queryString) throws IOException {
|
||||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||||
lenient().when(opener.openForRead("default")).thenReturn(reader);
|
|
||||||
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
||||||
LuceneSearchableType searchableType = resolver.resolve(type);
|
LuceneSearchableType searchableType = resolver.resolve(type);
|
||||||
|
lenient().when(opener.openForRead(searchableType, "default")).thenReturn(reader);
|
||||||
LuceneQueryBuilder<T> builder = new LuceneQueryBuilder<T>(
|
LuceneQueryBuilder<T> builder = new LuceneQueryBuilder<T>(
|
||||||
opener, "default", searchableType, new StandardAnalyzer()
|
opener, "default", searchableType, new StandardAnalyzer()
|
||||||
);
|
);
|
||||||
@@ -572,10 +550,11 @@ class LuceneQueryBuilderTest {
|
|||||||
|
|
||||||
private <T> QueryResult query(Class<?> type, String queryString, Integer start, Integer limit) throws IOException {
|
private <T> QueryResult query(Class<?> type, String queryString, Integer start, Integer limit) throws IOException {
|
||||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||||
lenient().when(opener.openForRead("default")).thenReturn(reader);
|
|
||||||
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
||||||
LuceneSearchableType searchableType = resolver.resolve(type);
|
LuceneSearchableType searchableType = resolver.resolve(type);
|
||||||
LuceneQueryBuilder<T> builder = new LuceneQueryBuilder<T>(
|
|
||||||
|
lenient().when(opener.openForRead(searchableType, "default")).thenReturn(reader);
|
||||||
|
LuceneQueryBuilder<T> builder = new LuceneQueryBuilder<>(
|
||||||
opener, "default", searchableType, new StandardAnalyzer()
|
opener, "default", searchableType, new StandardAnalyzer()
|
||||||
);
|
);
|
||||||
if (start != null) {
|
if (start != null) {
|
||||||
@@ -597,14 +576,14 @@ class LuceneQueryBuilderTest {
|
|||||||
private Document simpleDoc(String content) {
|
private Document simpleDoc(String content) {
|
||||||
Document document = new Document();
|
Document document = new Document();
|
||||||
document.add(new TextField("content", content, Field.Store.YES));
|
document.add(new TextField("content", content, Field.Store.YES));
|
||||||
document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
// document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Document permissionDoc(String content, String permission) {
|
private Document permissionDoc(String content, String permission) {
|
||||||
Document document = new Document();
|
Document document = new Document();
|
||||||
document.add(new TextField("content", content, Field.Store.YES));
|
document.add(new TextField("content", content, Field.Store.YES));
|
||||||
document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
// document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
||||||
document.add(new StringField(FieldNames.PERMISSION, permission, Field.Store.YES));
|
document.add(new StringField(FieldNames.PERMISSION, permission, Field.Store.YES));
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
@@ -612,7 +591,7 @@ class LuceneQueryBuilderTest {
|
|||||||
private Document repositoryDoc(String content, String repository) {
|
private Document repositoryDoc(String content, String repository) {
|
||||||
Document document = new Document();
|
Document document = new Document();
|
||||||
document.add(new TextField("content", content, Field.Store.YES));
|
document.add(new TextField("content", content, Field.Store.YES));
|
||||||
document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
// document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
||||||
document.add(new StringField(FieldNames.REPOSITORY, repository, Field.Store.YES));
|
document.add(new StringField(FieldNames.REPOSITORY, repository, Field.Store.YES));
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
@@ -624,14 +603,14 @@ class LuceneQueryBuilderTest {
|
|||||||
document.add(new TextField("displayName", displayName, Field.Store.YES));
|
document.add(new TextField("displayName", displayName, Field.Store.YES));
|
||||||
document.add(new TextField("carLicense", carLicense, Field.Store.YES));
|
document.add(new TextField("carLicense", carLicense, Field.Store.YES));
|
||||||
document.add(new StringField(FieldNames.ID, lastName, Field.Store.YES));
|
document.add(new StringField(FieldNames.ID, lastName, Field.Store.YES));
|
||||||
document.add(new StringField(FieldNames.TYPE, "inetOrgPerson", Field.Store.YES));
|
// document.add(new StringField(FieldNames.TYPE, "inetOrgPerson", Field.Store.YES));
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Document personDoc(String lastName) {
|
private Document personDoc(String lastName) {
|
||||||
Document document = new Document();
|
Document document = new Document();
|
||||||
document.add(new TextField("lastName", lastName, Field.Store.YES));
|
document.add(new TextField("lastName", lastName, Field.Store.YES));
|
||||||
document.add(new StringField(FieldNames.TYPE, "person", Field.Store.YES));
|
// document.add(new StringField(FieldNames.TYPE, "person", Field.Store.YES));
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,14 +623,14 @@ class LuceneQueryBuilderTest {
|
|||||||
document.add(new StringField("boolValue", String.valueOf(boolValue), Field.Store.YES));
|
document.add(new StringField("boolValue", String.valueOf(boolValue), Field.Store.YES));
|
||||||
document.add(new LongPoint("instantValue", instantValue.toEpochMilli()));
|
document.add(new LongPoint("instantValue", instantValue.toEpochMilli()));
|
||||||
document.add(new StoredField("instantValue", instantValue.toEpochMilli()));
|
document.add(new StoredField("instantValue", instantValue.toEpochMilli()));
|
||||||
document.add(new StringField(FieldNames.TYPE, "types", Field.Store.YES));
|
// document.add(new StringField(FieldNames.TYPE, "types", Field.Store.YES));
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Document denyDoc(String value) {
|
private Document denyDoc(String value) {
|
||||||
Document document = new Document();
|
Document document = new Document();
|
||||||
document.add(new TextField("value", value, Field.Store.YES));
|
document.add(new TextField("value", value, Field.Store.YES));
|
||||||
document.add(new StringField(FieldNames.TYPE, "deny", Field.Store.YES));
|
// document.add(new StringField(FieldNames.TYPE, "deny", Field.Store.YES));
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,20 @@ package sonia.scm.search;
|
|||||||
import org.apache.shiro.authz.AuthorizationException;
|
import org.apache.shiro.authz.AuthorizationException;
|
||||||
import org.github.sdorra.jse.ShiroExtension;
|
import org.github.sdorra.jse.ShiroExtension;
|
||||||
import org.github.sdorra.jse.SubjectAware;
|
import org.github.sdorra.jse.SubjectAware;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Answers;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.user.User;
|
||||||
|
import sonia.scm.work.CentralWorkQueue;
|
||||||
|
import sonia.scm.work.Task;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -46,176 +54,329 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@SubjectAware("trillian")
|
@SubjectAware("trillian")
|
||||||
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
|
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
|
||||||
class LuceneSearchEngineTest {
|
class LuceneSearchEngineTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IndexManager indexManager;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private SearchableTypeResolver resolver;
|
private SearchableTypeResolver resolver;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private IndexQueue indexQueue;
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private LuceneQueryBuilderFactory queryBuilderFactory;
|
private LuceneQueryBuilderFactory queryBuilderFactory;
|
||||||
|
|
||||||
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
|
private CentralWorkQueue centralWorkQueue;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private LuceneSearchEngine searchEngine;
|
private LuceneSearchEngine searchEngine;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private LuceneSearchableType searchableType;
|
private LuceneSearchableType searchableType;
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
void shouldDelegateGetSearchableTypes() {
|
class GetSearchableTypesTests {
|
||||||
List<LuceneSearchableType> mockedTypes = Collections.singletonList(searchableType("repository"));
|
|
||||||
when(resolver.getSearchableTypes()).thenReturn(mockedTypes);
|
|
||||||
|
|
||||||
Collection<SearchableType> searchableTypes = searchEngine.getSearchableTypes();
|
@Test
|
||||||
|
void shouldDelegateGetSearchableTypes() {
|
||||||
|
List<LuceneSearchableType> mockedTypes = Collections.singletonList(searchableType("repository"));
|
||||||
|
when(resolver.getSearchableTypes()).thenReturn(mockedTypes);
|
||||||
|
|
||||||
assertThat(searchableTypes).containsAll(mockedTypes);
|
Collection<SearchableType> searchableTypes = searchEngine.getSearchableTypes();
|
||||||
|
|
||||||
|
assertThat(searchableTypes).containsAll(mockedTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SubjectAware(value = "dent", permissions = "user:list")
|
||||||
|
void shouldExcludeTypesWithoutPermission() {
|
||||||
|
LuceneSearchableType repository = searchableType("repository");
|
||||||
|
LuceneSearchableType user = searchableType("user", "user:list");
|
||||||
|
LuceneSearchableType group = searchableType("group", "group:list");
|
||||||
|
List<LuceneSearchableType> mockedTypes = Arrays.asList(repository, user, group);
|
||||||
|
when(resolver.getSearchableTypes()).thenReturn(mockedTypes);
|
||||||
|
|
||||||
|
Collection<SearchableType> searchableTypes = searchEngine.getSearchableTypes();
|
||||||
|
|
||||||
|
assertThat(searchableTypes).containsOnly(repository, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LuceneSearchableType searchableType(String name) {
|
||||||
|
return searchableType(name, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LuceneSearchableType searchableType(String name, String permission) {
|
||||||
|
LuceneSearchableType searchableType = mock(LuceneSearchableType.class);
|
||||||
|
lenient().when(searchableType.getName()).thenReturn(name);
|
||||||
|
when(searchableType.getPermission()).thenReturn(Optional.ofNullable(permission));
|
||||||
|
return searchableType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
@SubjectAware(value = "dent", permissions = "user:list")
|
class SearchTests {
|
||||||
void shouldExcludeTypesWithoutPermission() {
|
|
||||||
LuceneSearchableType repository = searchableType("repository");
|
|
||||||
LuceneSearchableType user = searchableType("user", "user:list");
|
|
||||||
LuceneSearchableType group = searchableType("group", "group:list");
|
|
||||||
List<LuceneSearchableType> mockedTypes = Arrays.asList(repository, user, group);
|
|
||||||
when(resolver.getSearchableTypes()).thenReturn(mockedTypes);
|
|
||||||
|
|
||||||
Collection<SearchableType> searchableTypes = searchEngine.getSearchableTypes();
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void shouldDelegateSearchWithDefaults() {
|
||||||
|
LuceneQueryBuilder<Repository> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||||
|
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||||
|
|
||||||
assertThat(searchableTypes).containsOnly(repository, user);
|
IndexParams params = new IndexParams("default", searchableType, IndexOptions.defaults());
|
||||||
|
when(queryBuilderFactory.<Repository>create(params)).thenReturn(mockedBuilder);
|
||||||
|
|
||||||
|
QueryBuilder<Repository> queryBuilder = searchEngine.forType(Repository.class).search();
|
||||||
|
|
||||||
|
assertThat(queryBuilder).isSameAs(mockedBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void shouldDelegateSearch() {
|
||||||
|
IndexOptions options = IndexOptions.naturalLanguage(Locale.GERMAN);
|
||||||
|
|
||||||
|
LuceneQueryBuilder<Repository> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||||
|
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||||
|
|
||||||
|
IndexParams params = new IndexParams("idx", searchableType, options);
|
||||||
|
when(queryBuilderFactory.<Repository>create(params)).thenReturn(mockedBuilder);
|
||||||
|
|
||||||
|
QueryBuilder<Repository> queryBuilder = searchEngine.forType(Repository.class).withIndex("idx").withOptions(options).search();
|
||||||
|
|
||||||
|
assertThat(queryBuilder).isSameAs(mockedBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailWithoutRequiredPermission() {
|
||||||
|
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||||
|
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||||
|
|
||||||
|
SearchEngine.ForType<Repository> forType = searchEngine.forType(Repository.class);
|
||||||
|
assertThrows(AuthorizationException.class, forType::search);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@SubjectAware(permissions = "repository:read")
|
||||||
|
void shouldNotFailWithRequiredPermission() {
|
||||||
|
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||||
|
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||||
|
|
||||||
|
LuceneQueryBuilder<Object> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||||
|
when(queryBuilderFactory.create(any())).thenReturn(mockedBuilder);
|
||||||
|
|
||||||
|
SearchEngine.ForType<Repository> forType = searchEngine.forType(Repository.class);
|
||||||
|
assertThat(forType.search()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailWithTypeNameWithoutRequiredPermission() {
|
||||||
|
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||||
|
when(resolver.resolveByName("repository")).thenReturn(searchableType);
|
||||||
|
|
||||||
|
SearchEngine.ForType<Object> forType = searchEngine.forType("repository");
|
||||||
|
assertThrows(AuthorizationException.class, forType::search);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@SubjectAware(permissions = "repository:read")
|
||||||
|
void shouldNotFailWithTypeNameAndRequiredPermission() {
|
||||||
|
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||||
|
when(resolver.resolveByName("repository")).thenReturn(searchableType);
|
||||||
|
|
||||||
|
LuceneQueryBuilder<Object> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||||
|
when(queryBuilderFactory.create(any())).thenReturn(mockedBuilder);
|
||||||
|
|
||||||
|
SearchEngine.ForType<Object> forType = searchEngine.forType("repository");
|
||||||
|
assertThat(forType.search()).isNotNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private LuceneSearchableType searchableType(String name) {
|
@Nested
|
||||||
return searchableType(name, null);
|
class IndexTests {
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Task> taskCaptor;
|
||||||
|
|
||||||
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
|
private CentralWorkQueue.Enqueue enqueue;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
when(centralWorkQueue.append()).thenReturn(enqueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSubmitSimpleTask() {
|
||||||
|
mockType();
|
||||||
|
|
||||||
|
searchEngine.forType(Repository.class).update(index -> {});
|
||||||
|
|
||||||
|
verifyTaskSubmitted(LuceneSimpleIndexTask.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSubmitInjectingTask() {
|
||||||
|
mockType();
|
||||||
|
|
||||||
|
searchEngine.forType(Repository.class).update(DummyIndexTask.class);
|
||||||
|
|
||||||
|
verifyTaskSubmitted(LuceneInjectingIndexTask.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLockTypeAndDefaultIndex() {
|
||||||
|
mockType();
|
||||||
|
|
||||||
|
searchEngine.forType(Repository.class).update(DummyIndexTask.class);
|
||||||
|
|
||||||
|
verify(enqueue).locks("repository-default-index");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLockTypeAndIndex() {
|
||||||
|
mockType();
|
||||||
|
|
||||||
|
searchEngine.forType(Repository.class).withIndex("sample").update(DummyIndexTask.class);
|
||||||
|
|
||||||
|
verify(enqueue).locks("repository-sample-index");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLockSpecificResource() {
|
||||||
|
mockType();
|
||||||
|
|
||||||
|
searchEngine.forType(Repository.class).forResource("one").update(DummyIndexTask.class);
|
||||||
|
|
||||||
|
verify(enqueue).locks("repository-default-index", "one");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLockMultipleSpecificResources() {
|
||||||
|
mockType();
|
||||||
|
|
||||||
|
searchEngine.forType(Repository.class)
|
||||||
|
.forResource("one")
|
||||||
|
.forResource("two")
|
||||||
|
.update(DummyIndexTask.class);
|
||||||
|
|
||||||
|
verify(enqueue).locks("repository-default-index", "one");
|
||||||
|
verify(enqueue).locks("repository-default-index", "two");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBatchSimpleTask() {
|
||||||
|
mockDetails(new LuceneIndexDetails(Repository.class, "default"));
|
||||||
|
|
||||||
|
searchEngine.forIndices().batch(index -> {});
|
||||||
|
|
||||||
|
verifyTaskSubmitted(LuceneSimpleIndexTask.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBatchAndLock() {
|
||||||
|
mockDetails(new LuceneIndexDetails(Repository.class, "default"));
|
||||||
|
|
||||||
|
searchEngine.forIndices().batch(index -> {});
|
||||||
|
|
||||||
|
verify(enqueue).locks("repository-default-index");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBatchAndLockSpecificResource() {
|
||||||
|
mockDetails(new LuceneIndexDetails(Repository.class, "default"));
|
||||||
|
|
||||||
|
searchEngine.forIndices().forResource("one").batch(index -> {});
|
||||||
|
|
||||||
|
verify(enqueue).locks("repository-default-index", "one");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBatchAndLockMultipleSpecificResources() {
|
||||||
|
mockDetails(new LuceneIndexDetails(Repository.class, "default"));
|
||||||
|
|
||||||
|
searchEngine.forIndices().forResource("one").forResource("two").batch(index -> {});
|
||||||
|
|
||||||
|
verify(enqueue).locks("repository-default-index", "one");
|
||||||
|
verify(enqueue).locks("repository-default-index", "two");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBatchInjectingTask() {
|
||||||
|
mockDetails(new LuceneIndexDetails(Repository.class, "default"));
|
||||||
|
|
||||||
|
searchEngine.forIndices().batch(DummyIndexTask.class);
|
||||||
|
|
||||||
|
verifyTaskSubmitted(LuceneInjectingIndexTask.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBatchMultipleTasks() {
|
||||||
|
mockDetails(
|
||||||
|
new LuceneIndexDetails(Repository.class, "default"),
|
||||||
|
new LuceneIndexDetails(User.class, "default")
|
||||||
|
);
|
||||||
|
|
||||||
|
searchEngine.forIndices().batch(index -> {});
|
||||||
|
|
||||||
|
verify(enqueue.runAsAdmin(), times(2)).enqueue(any(Task.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFilterWithPredicate() {
|
||||||
|
mockDetails(
|
||||||
|
new LuceneIndexDetails(Repository.class, "default"),
|
||||||
|
new LuceneIndexDetails(User.class, "default")
|
||||||
|
);
|
||||||
|
|
||||||
|
searchEngine.forIndices()
|
||||||
|
.matching(details -> details.getType() == Repository.class)
|
||||||
|
.batch(index -> {});
|
||||||
|
|
||||||
|
verify(enqueue.runAsAdmin()).enqueue(any(Task.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends IndexDetails> void mockDetails(LuceneIndexDetails... details) {
|
||||||
|
for (LuceneIndexDetails detail : details) {
|
||||||
|
mockType(detail.getType());
|
||||||
|
}
|
||||||
|
when(indexManager.all()).thenAnswer(ic -> Arrays.asList(details));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyTaskSubmitted(Class<? extends Task> typeOfTask) {
|
||||||
|
verify(enqueue.runAsAdmin()).enqueue(taskCaptor.capture());
|
||||||
|
|
||||||
|
Task task = taskCaptor.getValue();
|
||||||
|
assertThat(task).isInstanceOf(typeOfTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockType() {
|
||||||
|
mockType(Repository.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockType(Class<?> type){
|
||||||
|
LuceneSearchableType searchableType = mock(LuceneSearchableType.class);
|
||||||
|
lenient().when(searchableType.getType()).thenAnswer(ic -> type);
|
||||||
|
lenient().when(searchableType.getName()).thenReturn(type.getSimpleName().toLowerCase(Locale.ENGLISH));
|
||||||
|
lenient().when(resolver.resolve(type)).thenReturn(searchableType);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private LuceneSearchableType searchableType(String name, String permission) {
|
|
||||||
LuceneSearchableType searchableType = mock(LuceneSearchableType.class);
|
|
||||||
lenient().when(searchableType.getName()).thenReturn(name);
|
|
||||||
when(searchableType.getPermission()).thenReturn(Optional.ofNullable(permission));
|
|
||||||
return searchableType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
public static class DummyIndexTask implements IndexTask<Repository> {
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
void shouldDelegateGetOrCreateWithDefaultIndex() {
|
|
||||||
Index<Repository> index = mock(Index.class);
|
|
||||||
|
|
||||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
@Override
|
||||||
IndexParams params = new IndexParams("default", searchableType, IndexOptions.defaults());
|
public void update(Index<Repository> index) {
|
||||||
when(indexQueue.<Repository>getQueuedIndex(params)).thenReturn(index);
|
|
||||||
|
|
||||||
Index<Repository> idx = searchEngine.forType(Repository.class).getOrCreate();
|
}
|
||||||
assertThat(idx).isSameAs(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
void shouldDelegateGetOrCreateIndexWithDefaults() {
|
|
||||||
Index<Repository> index = mock(Index.class);
|
|
||||||
|
|
||||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
|
||||||
IndexParams params = new IndexParams("idx", searchableType, IndexOptions.defaults());
|
|
||||||
when(indexQueue.<Repository>getQueuedIndex(params)).thenReturn(index);
|
|
||||||
|
|
||||||
Index<Repository> idx = searchEngine.forType(Repository.class).withIndex("idx").getOrCreate();
|
|
||||||
assertThat(idx).isSameAs(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
void shouldDelegateGetOrCreateIndex() {
|
|
||||||
Index<Repository> index = mock(Index.class);
|
|
||||||
IndexOptions options = IndexOptions.naturalLanguage(Locale.ENGLISH);
|
|
||||||
|
|
||||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
|
||||||
IndexParams params = new IndexParams("default", searchableType, options);
|
|
||||||
when(indexQueue.<Repository>getQueuedIndex(params)).thenReturn(index);
|
|
||||||
|
|
||||||
Index<Repository> idx = searchEngine.forType(Repository.class).withOptions(options).getOrCreate();
|
|
||||||
assertThat(idx).isSameAs(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
void shouldDelegateSearchWithDefaults() {
|
|
||||||
LuceneQueryBuilder<Repository> mockedBuilder = mock(LuceneQueryBuilder.class);
|
|
||||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
|
||||||
|
|
||||||
IndexParams params = new IndexParams("default", searchableType, IndexOptions.defaults());
|
|
||||||
when(queryBuilderFactory.<Repository>create(params)).thenReturn(mockedBuilder);
|
|
||||||
|
|
||||||
QueryBuilder<Repository> queryBuilder = searchEngine.forType(Repository.class).search();
|
|
||||||
|
|
||||||
assertThat(queryBuilder).isSameAs(mockedBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
void shouldDelegateSearch() {
|
|
||||||
IndexOptions options = IndexOptions.naturalLanguage(Locale.GERMAN);
|
|
||||||
|
|
||||||
LuceneQueryBuilder<Repository> mockedBuilder = mock(LuceneQueryBuilder.class);
|
|
||||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
|
||||||
|
|
||||||
IndexParams params = new IndexParams("idx", searchableType, options);
|
|
||||||
when(queryBuilderFactory.<Repository>create(params)).thenReturn(mockedBuilder);
|
|
||||||
|
|
||||||
QueryBuilder<Repository> queryBuilder = searchEngine.forType(Repository.class).withIndex("idx").withOptions(options).search();
|
|
||||||
|
|
||||||
assertThat(queryBuilder).isSameAs(mockedBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldFailWithoutRequiredPermission() {
|
|
||||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
|
||||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
|
||||||
|
|
||||||
SearchEngine.ForType<Repository> forType = searchEngine.forType(Repository.class);
|
|
||||||
assertThrows(AuthorizationException.class, forType::search);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@SubjectAware(permissions = "repository:read")
|
|
||||||
void shouldNotFailWithRequiredPermission() {
|
|
||||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
|
||||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
|
||||||
|
|
||||||
LuceneQueryBuilder<Object> mockedBuilder = mock(LuceneQueryBuilder.class);
|
|
||||||
when(queryBuilderFactory.create(any())).thenReturn(mockedBuilder);
|
|
||||||
|
|
||||||
SearchEngine.ForType<Repository> forType = searchEngine.forType(Repository.class);
|
|
||||||
assertThat(forType.search()).isNotNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldFailWithTypeNameWithoutRequiredPermission() {
|
|
||||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
|
||||||
when(resolver.resolveByName("repository")).thenReturn(searchableType);
|
|
||||||
|
|
||||||
SearchEngine.ForType<Object> forType = searchEngine.forType("repository");
|
|
||||||
assertThrows(AuthorizationException.class, forType::search);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@SubjectAware(permissions = "repository:read")
|
|
||||||
void shouldNotFailWithTypeNameAndRequiredPermission() {
|
|
||||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
|
||||||
when(resolver.resolveByName("repository")).thenReturn(searchableType);
|
|
||||||
|
|
||||||
LuceneQueryBuilder<Object> mockedBuilder = mock(LuceneQueryBuilder.class);
|
|
||||||
when(queryBuilderFactory.create(any())).thenReturn(mockedBuilder);
|
|
||||||
|
|
||||||
SearchEngine.ForType<Object> forType = searchEngine.forType("repository");
|
|
||||||
assertThat(forType.search()).isNotNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Guice;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class LuceneSimpleIndexTaskTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LuceneIndexFactory indexFactory;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LuceneIndex<Repository> index;
|
||||||
|
|
||||||
|
private final SearchableTypeResolver resolver = new SearchableTypeResolver(Repository.class);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdate() {
|
||||||
|
Injector injector = createInjector();
|
||||||
|
|
||||||
|
LuceneSearchableType searchableType = resolver.resolve(Repository.class);
|
||||||
|
|
||||||
|
IndexParams params = new IndexParams("default", searchableType, IndexOptions.defaults());
|
||||||
|
|
||||||
|
AtomicReference<Index<?>> ref = new AtomicReference<>();
|
||||||
|
LuceneSimpleIndexTask task = new LuceneSimpleIndexTask(params, ref::set);
|
||||||
|
injector.injectMembers(task);
|
||||||
|
|
||||||
|
when(indexFactory.create(params)).then(ic -> index);
|
||||||
|
|
||||||
|
task.run();
|
||||||
|
|
||||||
|
assertThat(index).isSameAs(ref.get());
|
||||||
|
verify(index).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Injector createInjector() {
|
||||||
|
return Guice.createInjector(new AbstractModule() {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(SearchableTypeResolver.class).toInstance(resolver);
|
||||||
|
bind(LuceneIndexFactory.class).toInstance(indexFactory);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import org.apache.lucene.document.Document;
|
||||||
|
import org.apache.lucene.index.IndexWriter;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.awaitility.Awaitility.await;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SharableIndexWriterTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IndexWriter underlyingWriter;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void shouldCreateIndexOnOpen() {
|
||||||
|
Supplier<IndexWriter> supplier = mock(Supplier.class);
|
||||||
|
|
||||||
|
SharableIndexWriter writer = new SharableIndexWriter(supplier);
|
||||||
|
verifyNoInteractions(supplier);
|
||||||
|
|
||||||
|
writer.open();
|
||||||
|
verify(supplier).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void shouldOpenWriterOnlyOnce() {
|
||||||
|
Supplier<IndexWriter> supplier = mock(Supplier.class);
|
||||||
|
|
||||||
|
SharableIndexWriter writer = new SharableIndexWriter(supplier);
|
||||||
|
writer.open();
|
||||||
|
writer.open();
|
||||||
|
writer.open();
|
||||||
|
|
||||||
|
verify(supplier).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldIncreaseUsageCounter() {
|
||||||
|
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||||
|
writer.open();
|
||||||
|
writer.open();
|
||||||
|
writer.open();
|
||||||
|
|
||||||
|
assertThat(writer.getUsageCounter()).isEqualTo(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDecreaseUsageCounter() throws IOException {
|
||||||
|
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||||
|
writer.open();
|
||||||
|
writer.open();
|
||||||
|
writer.open();
|
||||||
|
|
||||||
|
assertThat(writer.getUsageCounter()).isEqualTo(3);
|
||||||
|
|
||||||
|
writer.close();
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
assertThat(writer.getUsageCounter()).isOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotCloseWriterIfUsageCounterIsGreaterZero() throws IOException {
|
||||||
|
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||||
|
writer.open();
|
||||||
|
writer.open();
|
||||||
|
writer.open();
|
||||||
|
|
||||||
|
writer.close();
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
verify(underlyingWriter, never()).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCloseIfUsageCounterIsZero() throws IOException {
|
||||||
|
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||||
|
writer.open();
|
||||||
|
writer.open();
|
||||||
|
|
||||||
|
writer.close();
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
verify(underlyingWriter).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void shouldReOpen() throws IOException {
|
||||||
|
Supplier<IndexWriter> supplier = mock(Supplier.class);
|
||||||
|
when(supplier.get()).thenReturn(underlyingWriter);
|
||||||
|
|
||||||
|
SharableIndexWriter writer = new SharableIndexWriter(supplier);
|
||||||
|
writer.open();
|
||||||
|
|
||||||
|
writer.close();
|
||||||
|
writer.open();
|
||||||
|
|
||||||
|
verify(supplier, times(2)).get();
|
||||||
|
verify(underlyingWriter).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateUpdates() throws IOException {
|
||||||
|
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||||
|
writer.open();
|
||||||
|
|
||||||
|
Term term = new Term("field", "value");
|
||||||
|
Document document = new Document();
|
||||||
|
writer.updateDocument(term, document);
|
||||||
|
|
||||||
|
verify(underlyingWriter).updateDocument(term, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateDeleteAll() throws IOException {
|
||||||
|
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||||
|
writer.open();
|
||||||
|
|
||||||
|
writer.deleteAll();
|
||||||
|
|
||||||
|
verify(underlyingWriter).deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateDeletes() throws IOException {
|
||||||
|
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||||
|
writer.open();
|
||||||
|
|
||||||
|
Term term = new Term("field", "value");
|
||||||
|
writer.deleteDocuments(term);
|
||||||
|
|
||||||
|
verify(underlyingWriter).deleteDocuments(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class ConcurrencyTests {
|
||||||
|
|
||||||
|
private ExecutorService executorService;
|
||||||
|
|
||||||
|
private final AtomicInteger openCounter = new AtomicInteger();
|
||||||
|
private final AtomicInteger commitCounter = new AtomicInteger();
|
||||||
|
private final AtomicInteger closeCounter = new AtomicInteger();
|
||||||
|
|
||||||
|
private final AtomicInteger invocations = new AtomicInteger();
|
||||||
|
|
||||||
|
private SharableIndexWriter writer;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws IOException {
|
||||||
|
executorService = Executors.newFixedThreadPool(4);
|
||||||
|
writer = new SharableIndexWriter(() -> {
|
||||||
|
openCounter.incrementAndGet();
|
||||||
|
return underlyingWriter;
|
||||||
|
});
|
||||||
|
|
||||||
|
doAnswer(ic -> commitCounter.incrementAndGet()).when(underlyingWriter).commit();
|
||||||
|
doAnswer(ic -> closeCounter.incrementAndGet()).when(underlyingWriter).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("java:S2925") // sleep is ok to simulate some work
|
||||||
|
void shouldKeepIndexOpen() {
|
||||||
|
AtomicBoolean fail = new AtomicBoolean(false);
|
||||||
|
for (int i = 0; i < 50; i++) {
|
||||||
|
executorService.submit(() -> {
|
||||||
|
writer.open();
|
||||||
|
try {
|
||||||
|
Thread.sleep(25);
|
||||||
|
writer.deleteAll();
|
||||||
|
writer.close();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
fail.set(true);
|
||||||
|
} catch (IOException e) {
|
||||||
|
fail.set(true);
|
||||||
|
} finally {
|
||||||
|
invocations.incrementAndGet();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
executorService.shutdown();
|
||||||
|
|
||||||
|
await().atMost(2, TimeUnit.SECONDS).until(() -> invocations.get() == 50);
|
||||||
|
|
||||||
|
assertThat(fail.get()).isFalse();
|
||||||
|
|
||||||
|
// It should be one, but it is possible that tasks finish before new added to the queue.
|
||||||
|
// This behaviour depends heavily on the cpu's of the machine which executes this test.
|
||||||
|
assertThat(openCounter.get()).isPositive().isLessThan(10);
|
||||||
|
// should be 49, but see comment above
|
||||||
|
assertThat(commitCounter.get()).isGreaterThan(40);
|
||||||
|
// should be 1, but see comment above
|
||||||
|
assertThat(closeCounter.get()).isPositive().isLessThan(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.UnavailableSecurityManagerException;
|
||||||
|
import org.apache.shiro.mgt.DefaultSecurityManager;
|
||||||
|
import org.apache.shiro.mgt.SecurityManager;
|
||||||
|
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||||
|
import org.github.sdorra.jse.ShiroExtension;
|
||||||
|
import org.github.sdorra.jse.SubjectAware;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import sonia.scm.security.Impersonator.Session;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class ImpersonatorTest {
|
||||||
|
|
||||||
|
private SecurityManager securityManager = new DefaultSecurityManager();
|
||||||
|
|
||||||
|
private Impersonator impersonator;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
impersonator = new Impersonator(securityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBindAndRestoreNonWebThread() {
|
||||||
|
try (Session session = impersonator.impersonate(principal("dent"))) {
|
||||||
|
assertPrincipal("dent");
|
||||||
|
}
|
||||||
|
assertThrows(UnavailableSecurityManagerException.class, SecurityUtils::getSubject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private SimplePrincipalCollection principal(String principal) {
|
||||||
|
return new SimplePrincipalCollection(principal, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertPrincipal(String principal) {
|
||||||
|
assertThat(SecurityUtils.getSubject().getPrincipal()).isEqualTo(principal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@ExtendWith(ShiroExtension.class)
|
||||||
|
class WithSecurityManager {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SubjectAware("trillian")
|
||||||
|
void shouldBindAndRestoreWebThread() {
|
||||||
|
assertPrincipal("trillian");
|
||||||
|
|
||||||
|
try (Session session = impersonator.impersonate(principal("slarti"))) {
|
||||||
|
assertPrincipal("slarti");
|
||||||
|
}
|
||||||
|
|
||||||
|
assertPrincipal("trillian");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.update.index;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class RemoveCombinedIndexTest {
|
||||||
|
|
||||||
|
private Path home;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SCMContextProvider contextProvider;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private RemoveCombinedIndex updateStep;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(@TempDir Path home) {
|
||||||
|
this.home = home;
|
||||||
|
when(contextProvider.resolve(any())).then(
|
||||||
|
ic -> home.resolve(ic.getArgument(0, Path.class))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRemoveIndexDirectory() throws IOException {
|
||||||
|
Path indexDirectory = home.resolve("index");
|
||||||
|
Path specificIndexDirectory = indexDirectory.resolve("repository").resolve("default");
|
||||||
|
Files.createDirectories(specificIndexDirectory);
|
||||||
|
Path helloTxt = specificIndexDirectory.resolve("hello.txt");
|
||||||
|
Files.write(helloTxt, "hello".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
assertThat(helloTxt).doesNotExist();
|
||||||
|
assertThat(indexDirectory).doesNotExist();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRemoveIndexLogDirectory() throws IOException {
|
||||||
|
Path logDirectory = home.resolve("var").resolve("data").resolve("index-log");
|
||||||
|
Files.createDirectories(logDirectory);
|
||||||
|
Path helloXml = logDirectory.resolve("hello.xml");
|
||||||
|
Files.write(helloXml, "<hello>world</hello>".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
assertThat(helloXml).doesNotExist();
|
||||||
|
assertThat(logDirectory).doesNotExist();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -24,20 +24,23 @@
|
|||||||
|
|
||||||
package sonia.scm.user;
|
package sonia.scm.user;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.HandlerEventType;
|
import sonia.scm.HandlerEventType;
|
||||||
import sonia.scm.search.Id;
|
import sonia.scm.search.Id;
|
||||||
import sonia.scm.search.Index;
|
import sonia.scm.search.Index;
|
||||||
|
import sonia.scm.search.IndexLogStore;
|
||||||
import sonia.scm.search.SearchEngine;
|
import sonia.scm.search.SearchEngine;
|
||||||
|
import sonia.scm.search.SerializableIndexTask;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static java.util.Collections.singletonList;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -54,6 +57,16 @@ class UserIndexerTest {
|
|||||||
@InjectMocks
|
@InjectMocks
|
||||||
private UserIndexer indexer;
|
private UserIndexer indexer;
|
||||||
|
|
||||||
|
|
||||||
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
|
private Index<User> index;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IndexLogStore indexLogStore;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<SerializableIndexTask<User>> captor;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnType() {
|
void shouldReturnType() {
|
||||||
assertThat(indexer.getType()).isEqualTo(User.class);
|
assertThat(indexer.getType()).isEqualTo(User.class);
|
||||||
@@ -64,58 +77,54 @@ class UserIndexerTest {
|
|||||||
assertThat(indexer.getVersion()).isEqualTo(UserIndexer.VERSION);
|
assertThat(indexer.getVersion()).isEqualTo(UserIndexer.VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
|
||||||
class UpdaterTests {
|
|
||||||
|
|
||||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
@Test
|
||||||
private Index<User> index;
|
void shouldReturnReIndexAllClass() {
|
||||||
|
assertThat(indexer.getReIndexAllTask()).isEqualTo(UserIndexer.ReIndexAll.class);
|
||||||
|
}
|
||||||
|
|
||||||
private final User user = UserTestData.createTrillian();
|
@Test
|
||||||
|
void shouldCreateUser() {
|
||||||
|
User trillian = UserTestData.createTrillian();
|
||||||
|
|
||||||
@BeforeEach
|
indexer.createStoreTask(trillian).update(index);
|
||||||
void open() {
|
|
||||||
when(searchEngine.forType(User.class).getOrCreate()).thenReturn(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
verify(index).store(Id.of(trillian), UserPermissions.read(trillian).asShiroString(), trillian);
|
||||||
void shouldStore() {
|
}
|
||||||
indexer.open().store(user);
|
|
||||||
|
|
||||||
verify(index).store(Id.of(user), "user:read:trillian", user);
|
@Test
|
||||||
}
|
void shouldDeleteUser() {
|
||||||
|
User trillian = UserTestData.createTrillian();
|
||||||
|
|
||||||
@Test
|
indexer.createDeleteTask(trillian).update(index);
|
||||||
void shouldDeleteById() {
|
|
||||||
indexer.open().delete(user);
|
|
||||||
|
|
||||||
verify(index.delete().byType()).byId(Id.of(user));
|
verify(index.delete()).byId(Id.of(trillian));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReIndexAll() {
|
void shouldReIndexAll() {
|
||||||
when(userManager.getAll()).thenReturn(singletonList(user));
|
User trillian = UserTestData.createTrillian();
|
||||||
|
User slarti = UserTestData.createSlarti();
|
||||||
|
when(userManager.getAll()).thenReturn(Arrays.asList(trillian, slarti));
|
||||||
|
|
||||||
indexer.open().reIndexAll();
|
UserIndexer.ReIndexAll reIndexAll = new UserIndexer.ReIndexAll(indexLogStore, userManager);
|
||||||
|
reIndexAll.update(index);
|
||||||
|
|
||||||
verify(index.delete().byType()).all();
|
verify(index.delete()).all();
|
||||||
verify(index).store(Id.of(user), "user:read:trillian", user);
|
verify(index).store(Id.of(trillian), UserPermissions.read(trillian).asShiroString(), trillian);
|
||||||
}
|
verify(index).store(Id.of(slarti), UserPermissions.read(slarti).asShiroString(), slarti);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldHandleEvent() {
|
void shouldHandleEvents() {
|
||||||
UserEvent event = new UserEvent(HandlerEventType.DELETE, user);
|
User trillian = UserTestData.createTrillian();
|
||||||
|
UserEvent event = new UserEvent(HandlerEventType.DELETE, trillian);
|
||||||
|
|
||||||
indexer.handleEvent(event);
|
indexer.handleEvent(event);
|
||||||
|
|
||||||
verify(index.delete().byType()).byId(Id.of(user));
|
verify(searchEngine.forType(User.class)).update(captor.capture());
|
||||||
}
|
captor.getValue().update(index);
|
||||||
|
verify(index.delete()).byId(Id.of(trillian));
|
||||||
@Test
|
|
||||||
void shouldCloseIndex() {
|
|
||||||
indexer.open().close();
|
|
||||||
|
|
||||||
verify(index).close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.security.Authentications;
|
import sonia.scm.security.Authentications;
|
||||||
|
import sonia.scm.security.Impersonator;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -54,7 +55,7 @@ class DefaultAdministrationContextTest {
|
|||||||
Injector injector = Guice.createInjector();
|
Injector injector = Guice.createInjector();
|
||||||
SecurityManager securityManager = new DefaultSecurityManager();
|
SecurityManager securityManager = new DefaultSecurityManager();
|
||||||
|
|
||||||
context = new DefaultAdministrationContext(injector, securityManager);
|
context = new DefaultAdministrationContext(injector, new Impersonator(securityManager));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,375 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Guice;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import io.micrometer.core.instrument.Meter;
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
|
import io.micrometer.core.instrument.Timer;
|
||||||
|
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||||
|
import org.apache.shiro.mgt.SecurityManager;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.subject.PrincipalCollection;
|
||||||
|
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||||
|
import org.github.sdorra.jse.ShiroExtension;
|
||||||
|
import org.github.sdorra.jse.SubjectAware;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.security.Authentications;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.awaitility.Awaitility.await;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@SubjectAware("trillian")
|
||||||
|
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
|
||||||
|
class DefaultCentralWorkQueueTest {
|
||||||
|
|
||||||
|
private final PrincipalCollection principal = new SimplePrincipalCollection("trillian", "test");
|
||||||
|
|
||||||
|
private static final int ITERATIONS = 50;
|
||||||
|
private static final int TIMEOUT = 1; // seconds
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Persistence persistence;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithDefaultInjector {
|
||||||
|
|
||||||
|
private MeterRegistry meterRegistry;
|
||||||
|
private DefaultCentralWorkQueue queue;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
meterRegistry = new SimpleMeterRegistry();
|
||||||
|
queue = new DefaultCentralWorkQueue(Guice.createInjector(new SecurityModule()), persistence, meterRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final AtomicInteger runs = new AtomicInteger();
|
||||||
|
private int counter = 0;
|
||||||
|
private int copy = -1;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRunInSequenceWithBlock() {
|
||||||
|
for (int i = 0; i < ITERATIONS; i++) {
|
||||||
|
queue.append().locks("counter").enqueue(new Increase());
|
||||||
|
}
|
||||||
|
waitForTasks();
|
||||||
|
|
||||||
|
assertThat(counter).isEqualTo(ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitForTasks() {
|
||||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> queue.getSize() == 0);
|
||||||
|
assertThat(runs.get()).isEqualTo(ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRunInParallel() {
|
||||||
|
for (int i = 0; i < ITERATIONS; i++) {
|
||||||
|
queue.append().enqueue(new Increase());
|
||||||
|
}
|
||||||
|
waitForTasks();
|
||||||
|
|
||||||
|
// we test if the resulting counter is less than the iteration,
|
||||||
|
// because it is extremely likely that we miss a counter update
|
||||||
|
// when we run in parallel
|
||||||
|
assertThat(counter)
|
||||||
|
.isPositive()
|
||||||
|
.isLessThan(ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotBlocked() {
|
||||||
|
for (int i = 0; i < ITERATIONS; i++) {
|
||||||
|
queue.append().locks("counter").enqueue(new Increase());
|
||||||
|
}
|
||||||
|
queue.append().enqueue(() -> copy = counter);
|
||||||
|
waitForTasks();
|
||||||
|
|
||||||
|
assertThat(counter).isEqualTo(ITERATIONS);
|
||||||
|
assertThat(copy).isNotNegative().isLessThan(ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotBlockedByDifferentResource() {
|
||||||
|
for (int i = 0; i < ITERATIONS; i++) {
|
||||||
|
queue.append().locks("counter").enqueue(new Increase());
|
||||||
|
}
|
||||||
|
queue.append().locks("copy").enqueue(() -> copy = counter);
|
||||||
|
waitForTasks();
|
||||||
|
|
||||||
|
assertThat(counter).isEqualTo(ITERATIONS);
|
||||||
|
assertThat(copy).isNotNegative().isLessThan(ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBeBlockedByParentResource() {
|
||||||
|
for (int i = 0; i < ITERATIONS; i++) {
|
||||||
|
queue.append().locks("counter").enqueue(new Increase());
|
||||||
|
}
|
||||||
|
queue.append().locks("counter", "one").enqueue(() -> copy = counter);
|
||||||
|
waitForTasks();
|
||||||
|
|
||||||
|
assertThat(counter).isEqualTo(ITERATIONS);
|
||||||
|
assertThat(copy).isEqualTo(ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBeBlockedByParentAndExactResource() {
|
||||||
|
for (int i = 0; i < ITERATIONS; i++) {
|
||||||
|
if (i % 2 == 0) {
|
||||||
|
queue.append().locks("counter", "c").enqueue(new Increase());
|
||||||
|
} else {
|
||||||
|
queue.append().locks("counter").enqueue(new Increase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
waitForTasks();
|
||||||
|
assertThat(counter).isEqualTo(ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBeBlockedByParentResourceWithModelObject() {
|
||||||
|
Repository one = repository("one");
|
||||||
|
for (int i = 0; i < ITERATIONS; i++) {
|
||||||
|
queue.append().locks("counter").enqueue(new Increase());
|
||||||
|
}
|
||||||
|
queue.append().locks("counter", one).enqueue(() -> copy = counter);
|
||||||
|
waitForTasks();
|
||||||
|
|
||||||
|
assertThat(counter).isEqualTo(ITERATIONS);
|
||||||
|
assertThat(copy).isEqualTo(ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFinalizeOnError() {
|
||||||
|
queue.append().enqueue(() -> {
|
||||||
|
throw new IllegalStateException("failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> queue.getSize() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSetThreadName() {
|
||||||
|
AtomicReference<String> threadName = new AtomicReference<>();
|
||||||
|
queue.append().enqueue(() -> threadName.set(Thread.currentThread().getName()));
|
||||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> threadName.get() != null);
|
||||||
|
|
||||||
|
assertThat(threadName.get()).startsWith("CentralWorkQueue");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCaptureExecutorMetrics() {
|
||||||
|
for (int i = 0; i < ITERATIONS; i++) {
|
||||||
|
queue.append().enqueue(new Increase());
|
||||||
|
}
|
||||||
|
waitForTasks();
|
||||||
|
|
||||||
|
double count = meterRegistry.get("executor.completed").functionCounter().count();
|
||||||
|
assertThat(count).isEqualTo(ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCaptureExecutionDuration() {
|
||||||
|
queue.append().enqueue(new Increase());
|
||||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> queue.getSize() == 0);
|
||||||
|
|
||||||
|
Timer timer = meterRegistry.get(UnitOfWork.METRIC_EXECUTION).timer();
|
||||||
|
assertThat(timer.count()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCaptureWaitDuration() {
|
||||||
|
queue.append().enqueue(new Increase());
|
||||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> queue.getSize() == 0);
|
||||||
|
|
||||||
|
Timer timer = meterRegistry.get(UnitOfWork.METRIC_WAIT).timer();
|
||||||
|
assertThat(timer.count()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldIncreaseBlockCount() {
|
||||||
|
for (int i = 0; i < ITERATIONS; i++) {
|
||||||
|
queue.append().locks("counter").enqueue(new Increase());
|
||||||
|
}
|
||||||
|
waitForTasks();
|
||||||
|
|
||||||
|
int blockCount = 0;
|
||||||
|
for (Meter meter : meterRegistry.getMeters()) {
|
||||||
|
Meter.Id id = meter.getId();
|
||||||
|
if ("cwq.task.wait.duration".equals(id.getName())) {
|
||||||
|
String blocked = id.getTag("blocked");
|
||||||
|
if (blocked != null) {
|
||||||
|
blockCount += Integer.parseInt(blocked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(blockCount).isPositive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Repository repository(String id) {
|
||||||
|
Repository one = new Repository();
|
||||||
|
one.setId(id);
|
||||||
|
return one;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
queue.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Increase implements Task {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("java:S2925")
|
||||||
|
public void run() {
|
||||||
|
int currentCounter = counter;
|
||||||
|
runs.incrementAndGet();
|
||||||
|
try {
|
||||||
|
Thread.sleep(5);
|
||||||
|
counter = currentCounter + 1;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldInjectDependencies() {
|
||||||
|
Context ctx = new Context();
|
||||||
|
DefaultCentralWorkQueue queue = new DefaultCentralWorkQueue(
|
||||||
|
Guice.createInjector(new SecurityModule(), binder -> binder.bind(Context.class).toInstance(ctx)),
|
||||||
|
persistence,
|
||||||
|
new SimpleMeterRegistry(),
|
||||||
|
() -> 2
|
||||||
|
);
|
||||||
|
|
||||||
|
queue.append().enqueue(InjectingTask.class);
|
||||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> ctx.value != null);
|
||||||
|
assertThat(ctx.value).isEqualTo("Hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLoadFromPersistence() {
|
||||||
|
Context context = new Context();
|
||||||
|
SimpleUnitOfWork one = new SimpleUnitOfWork(
|
||||||
|
21L, principal, Collections.singleton(new Resource("a")), new InjectingTask(context, "one")
|
||||||
|
);
|
||||||
|
SimpleUnitOfWork two = new SimpleUnitOfWork(
|
||||||
|
42L, principal, Collections.singleton(new Resource("a")), new InjectingTask(context, "two")
|
||||||
|
);
|
||||||
|
two.restore(42L);
|
||||||
|
when(persistence.loadAll()).thenReturn(Arrays.asList(one, two));
|
||||||
|
|
||||||
|
new DefaultCentralWorkQueue(Guice.createInjector(new SecurityModule()), persistence, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> context.value != null);
|
||||||
|
assertThat(context.value).isEqualTo("two");
|
||||||
|
assertThat(one.getOrder()).isEqualTo(1L);
|
||||||
|
assertThat(one.getRestoreCount()).isEqualTo(1);
|
||||||
|
assertThat(two.getOrder()).isEqualTo(2L);
|
||||||
|
assertThat(two.getRestoreCount()).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRunAsUser() {
|
||||||
|
DefaultCentralWorkQueue workQueue = new DefaultCentralWorkQueue(
|
||||||
|
Guice.createInjector(new SecurityModule()), persistence, new SimpleMeterRegistry()
|
||||||
|
);
|
||||||
|
|
||||||
|
AtomicReference<Object> ref = new AtomicReference<>();
|
||||||
|
workQueue.append().enqueue(() -> ref.set(SecurityUtils.getSubject().getPrincipal()));
|
||||||
|
await().atMost(1, TimeUnit.SECONDS).until(() -> "trillian".equals(ref.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRunAsAdminUser() {
|
||||||
|
DefaultCentralWorkQueue workQueue = new DefaultCentralWorkQueue(
|
||||||
|
Guice.createInjector(new SecurityModule()), persistence, new SimpleMeterRegistry()
|
||||||
|
);
|
||||||
|
|
||||||
|
AtomicReference<Object> ref = new AtomicReference<>();
|
||||||
|
workQueue.append().runAsAdmin().enqueue(() -> ref.set(SecurityUtils.getSubject().getPrincipal()));
|
||||||
|
await().atMost(1, TimeUnit.SECONDS).until(() -> Authentications.PRINCIPAL_SYSTEM.equals(ref.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Context {
|
||||||
|
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class InjectingTask implements Task {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public InjectingTask(Context context) {
|
||||||
|
this(context, "Hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
public InjectingTask(Context context, String value) {
|
||||||
|
this.context = context;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
context.setValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SecurityModule extends AbstractModule {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(SecurityManager.class).toInstance(SecurityUtils.getSecurityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
178
scm-webapp/src/test/java/sonia/scm/work/PersistenceTest.java
Normal file
178
scm-webapp/src/test/java/sonia/scm/work/PersistenceTest.java
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.apache.shiro.subject.PrincipalCollection;
|
||||||
|
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
import sonia.scm.store.Blob;
|
||||||
|
import sonia.scm.store.InMemoryBlobStore;
|
||||||
|
import sonia.scm.store.InMemoryBlobStoreFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PersistenceTest {
|
||||||
|
|
||||||
|
private final PrincipalCollection principal = new SimplePrincipalCollection("trillian", "test");
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class Default {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PluginLoader pluginLoader;
|
||||||
|
|
||||||
|
private Persistence persistence;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
when(pluginLoader.getUberClassLoader()).thenReturn(PersistenceTest.class.getClassLoader());
|
||||||
|
persistence = new Persistence(pluginLoader, new InMemoryBlobStoreFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldStoreSimpleChunkOfWork() {
|
||||||
|
UnitOfWork work = new SimpleUnitOfWork(
|
||||||
|
1L, principal, Collections.singleton(new Resource("a")), new MyTask()
|
||||||
|
);
|
||||||
|
persistence.store(work);
|
||||||
|
|
||||||
|
UnitOfWork loaded = persistence.loadAll().iterator().next();
|
||||||
|
assertThat(loaded).isEqualTo(work);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldStoreInjectingChunkOfWork() {
|
||||||
|
UnitOfWork work = new InjectingUnitOfWork(
|
||||||
|
1L, principal, Collections.singleton(new Resource("a")), MyTask.class
|
||||||
|
);
|
||||||
|
persistence.store(work);
|
||||||
|
|
||||||
|
UnitOfWork loaded = persistence.loadAll().iterator().next();
|
||||||
|
assertThat(loaded).isEqualTo(work);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLoadInOrder() {
|
||||||
|
store(5, 3, 1, 4, 2);
|
||||||
|
|
||||||
|
long[] orderIds = persistence.loadAll()
|
||||||
|
.stream()
|
||||||
|
.mapToLong(UnitOfWork::getOrder)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
assertThat(orderIds).containsExactly(1, 2, 3, 4, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRemoveAfterLoad() {
|
||||||
|
store(1, 2);
|
||||||
|
|
||||||
|
assertThat(persistence.loadAll()).hasSize(2);
|
||||||
|
assertThat(persistence.loadAll()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailIfNotSerializable() {
|
||||||
|
store(1);
|
||||||
|
|
||||||
|
SimpleUnitOfWork unitOfWork = new SimpleUnitOfWork(
|
||||||
|
2L, principal, Collections.emptySet(), new NotSerializable()
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThrows(NonPersistableTaskException.class, () -> persistence.store(unitOfWork));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRemoveStored() {
|
||||||
|
store(1);
|
||||||
|
SimpleUnitOfWork chunkOfWork = new SimpleUnitOfWork(
|
||||||
|
2L, principal, Collections.emptySet(), new MyTask()
|
||||||
|
);
|
||||||
|
persistence.store(chunkOfWork);
|
||||||
|
persistence.remove(chunkOfWork);
|
||||||
|
|
||||||
|
assertThat(persistence.loadAll()).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void store(long... orderIds) {
|
||||||
|
for (long order : orderIds) {
|
||||||
|
persistence.store(new SimpleUnitOfWork(
|
||||||
|
order, principal, Collections.emptySet(), new MyTask()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFailForNonChunkOfWorkItems() throws IOException {
|
||||||
|
InMemoryBlobStore blobStore = new InMemoryBlobStore();
|
||||||
|
|
||||||
|
Persistence persistence = new Persistence(PersistenceTest.class.getClassLoader(), blobStore);
|
||||||
|
persistence.store(new SimpleUnitOfWork(
|
||||||
|
1L, principal, Collections.emptySet(), new MyTask())
|
||||||
|
);
|
||||||
|
|
||||||
|
Blob blob = blobStore.create();
|
||||||
|
try (ObjectOutputStream stream = new ObjectOutputStream(blob.getOutputStream())) {
|
||||||
|
stream.writeObject(new MyTask());
|
||||||
|
blob.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(persistence.loadAll()).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public static class MyTask implements Task {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// non static inner classes are not serializable
|
||||||
|
private class NotSerializable implements Task {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
72
scm-webapp/src/test/java/sonia/scm/work/ResourceTest.java
Normal file
72
scm-webapp/src/test/java/sonia/scm/work/ResourceTest.java
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class ResourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnResourceName() {
|
||||||
|
assertThat(res("a")).hasToString("a");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnResourceNameAndId() {
|
||||||
|
assertThat(res("a", "b")).hasToString("a:b");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class IsBlockedByTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnTrue() {
|
||||||
|
assertThat(res("a").isBlockedBy(res("a"))).isTrue();
|
||||||
|
assertThat(res("a", "b").isBlockedBy(res("a", "b"))).isTrue();
|
||||||
|
assertThat(res("a").isBlockedBy(res("a", "b"))).isTrue();
|
||||||
|
assertThat(res("a", "b").isBlockedBy(res("a"))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnFalse() {
|
||||||
|
assertThat(res("a").isBlockedBy(res("b"))).isFalse();
|
||||||
|
assertThat(res("a", "b").isBlockedBy(res("a", "c"))).isFalse();
|
||||||
|
assertThat(res("a", "b").isBlockedBy(res("c", "b"))).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Resource res(String name) {
|
||||||
|
return new Resource(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Resource res(String name, String id) {
|
||||||
|
return new Resource(name, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Guice;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import lombok.Value;
|
||||||
|
import org.apache.shiro.subject.PrincipalCollection;
|
||||||
|
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class SimpleUnitOfWorkTest {
|
||||||
|
|
||||||
|
private PrincipalCollection principal = new SimplePrincipalCollection("trillian", "test");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldInjectMember() {
|
||||||
|
Context context = new Context("awesome");
|
||||||
|
Injector injector = Guice.createInjector(new AbstractModule() {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(Context.class).toInstance(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SimpleTask simpleTask = new SimpleTask();
|
||||||
|
SimpleUnitOfWork unitOfWork = new SimpleUnitOfWork(1L, principal, Collections.emptySet(), simpleTask);
|
||||||
|
unitOfWork.task(injector);
|
||||||
|
|
||||||
|
simpleTask.run();
|
||||||
|
|
||||||
|
assertThat(simpleTask.value).isEqualTo("awesome");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public class Context {
|
||||||
|
String value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SimpleTask implements Task {
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
private String value = "no value set";
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public void setContext(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (context != null) {
|
||||||
|
value = context.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.work;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ThreadCountProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseTwoWorkersForOneCPU() {
|
||||||
|
ThreadCountProvider provider = new ThreadCountProvider(() -> 1);
|
||||||
|
|
||||||
|
assertThat(provider.getAsInt()).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "shouldUseFourWorkersFor{argumentsWithNames}CPU")
|
||||||
|
@ValueSource(ints = {2, 4, 8, 16})
|
||||||
|
void shouldUseFourWorkersForMoreThanOneCPU(int cpus) {
|
||||||
|
ThreadCountProvider provider = new ThreadCountProvider(() -> cpus);
|
||||||
|
|
||||||
|
assertThat(provider.getAsInt()).isEqualTo(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class SystemPropertyTests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
System.clearProperty(ThreadCountProvider.PROPERTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseCountFromSystemProperty() {
|
||||||
|
ThreadCountProvider provider = new ThreadCountProvider();
|
||||||
|
System.setProperty(ThreadCountProvider.PROPERTY, "6");
|
||||||
|
assertThat(provider.getAsInt()).isEqualTo(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {"-1", "0", "100", "a", ""})
|
||||||
|
void shouldUseDefaultForInvalidValue(String value) {
|
||||||
|
ThreadCountProvider provider = new ThreadCountProvider(() -> 1);
|
||||||
|
System.setProperty(ThreadCountProvider.PROPERTY, value);
|
||||||
|
assertThat(provider.getAsInt()).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user