mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-03 03:55:51 +01:00
Prepare search api for different types (#1732)
We introduced a new annotation '@IndexedType' which gets collected by the scm-annotation-processor. All classes which are annotated are index and searchable. This opens the search api for plugins.
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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 sonia.scm.plugin.PluginAnnotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark an field object should be indexed.
|
||||||
|
*
|
||||||
|
* @since 2.21.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
@Documented
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@PluginAnnotation("indexed-type")
|
||||||
|
public @interface IndexedType {
|
||||||
|
/**
|
||||||
|
* Returns the name of the indexed object.
|
||||||
|
* Default is the simple name of the indexed class, with the first char is lowercase.
|
||||||
|
*
|
||||||
|
* @return name of the index object or an empty string which indicates that the default should be used.
|
||||||
|
*/
|
||||||
|
String value() default "";
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ package sonia.scm.plugin;
|
|||||||
|
|
||||||
import com.google.inject.Binder;
|
import com.google.inject.Binder;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process and resolve extensions.
|
* Process and resolve extensions.
|
||||||
*
|
*
|
||||||
@@ -46,8 +48,7 @@ public interface ExtensionProcessor
|
|||||||
*
|
*
|
||||||
* @return extensions
|
* @return extensions
|
||||||
*/
|
*/
|
||||||
public <T> Iterable<Class<? extends T>> byExtensionPoint(
|
<T> Iterable<Class<? extends T>> byExtensionPoint(Class<T> extensionPoint);
|
||||||
Class<T> extensionPoint);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns single extension by its extension point.
|
* Returns single extension by its extension point.
|
||||||
@@ -58,7 +59,7 @@ public interface ExtensionProcessor
|
|||||||
*
|
*
|
||||||
* @return extension
|
* @return extension
|
||||||
*/
|
*/
|
||||||
public <T> Class<? extends T> oneByExtensionPoint(Class<T> extensionPoint);
|
<T> Class<? extends T> oneByExtensionPoint(Class<T> extensionPoint);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process auto bind extensions.
|
* Process auto bind extensions.
|
||||||
@@ -66,7 +67,7 @@ public interface ExtensionProcessor
|
|||||||
*
|
*
|
||||||
* @param binder injection binder
|
* @param binder injection binder
|
||||||
*/
|
*/
|
||||||
public void processAutoBindExtensions(Binder binder);
|
void processAutoBindExtensions(Binder binder);
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
|
|
||||||
@@ -76,5 +77,15 @@ public interface ExtensionProcessor
|
|||||||
*
|
*
|
||||||
* @return collected web elements
|
* @return collected web elements
|
||||||
*/
|
*/
|
||||||
public Iterable<WebElementExtension> getWebElements();
|
Iterable<WebElementExtension> getWebElements();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all collected indexable types.
|
||||||
|
*
|
||||||
|
* @return collected indexable types
|
||||||
|
* @since 2.21.0
|
||||||
|
*/
|
||||||
|
default Iterable<Class<?>> getIndexedTypes() {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,18 +26,15 @@ package sonia.scm.plugin;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import com.google.common.base.Function;
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Iterables;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import javax.xml.bind.annotation.XmlAccessType;
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
import javax.xml.bind.annotation.XmlElement;
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
import javax.xml.bind.annotation.XmlRootElement;
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -127,6 +124,10 @@ public class ScmModule
|
|||||||
return nonNull(webElements);
|
return nonNull(webElements);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Iterable<ClassElement> getIndexedTypes() {
|
||||||
|
return nonNull(indexedTypes);
|
||||||
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,6 +152,9 @@ public class ScmModule
|
|||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
|
@XmlElement(name = "indexed-type")
|
||||||
|
private Set<ClassElement> indexedTypes;
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
@XmlElement(name = "event")
|
@XmlElement(name = "event")
|
||||||
private Set<ClassElement> events;
|
private Set<ClassElement> events;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.google.common.base.Objects;
|
|||||||
import sonia.scm.BasicPropertiesAware;
|
import sonia.scm.BasicPropertiesAware;
|
||||||
import sonia.scm.ModelObject;
|
import sonia.scm.ModelObject;
|
||||||
import sonia.scm.search.Indexed;
|
import sonia.scm.search.Indexed;
|
||||||
|
import sonia.scm.search.IndexedType;
|
||||||
import sonia.scm.util.Util;
|
import sonia.scm.util.Util;
|
||||||
import sonia.scm.util.ValidationUtil;
|
import sonia.scm.util.ValidationUtil;
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ import java.util.Set;
|
|||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
|
@IndexedType
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
@XmlRootElement(name = "repositories")
|
@XmlRootElement(name = "repositories")
|
||||||
@StaticPermissions(
|
@StaticPermissions(
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ public interface Index extends AutoCloseable {
|
|||||||
*/
|
*/
|
||||||
void deleteByType(Class<?> type);
|
void deleteByType(Class<?> type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all objects with the given type from index.
|
||||||
|
* This method is mostly if the index type has changed and the old type (in form of class) is no longer available.
|
||||||
|
* @param typeName type name of objects
|
||||||
|
*/
|
||||||
|
void deleteByTypeName(String typeName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close index and commit changes.
|
* Close index and commit changes.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,10 +26,15 @@ package sonia.scm.search;
|
|||||||
|
|
||||||
import com.google.common.annotations.Beta;
|
import com.google.common.annotations.Beta;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
import sonia.scm.ContextEntry;
|
||||||
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
import static sonia.scm.NotFoundException.notFound;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build and execute queries against an index.
|
* Build and execute queries against an index.
|
||||||
*
|
*
|
||||||
@@ -42,6 +47,7 @@ public abstract class QueryBuilder {
|
|||||||
private int start = 0;
|
private int start = 0;
|
||||||
private int limit = 10;
|
private int limit = 10;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return only results which are related to the given repository.
|
* Return only results which are related to the given repository.
|
||||||
* @param repository repository
|
* @param repository repository
|
||||||
@@ -93,6 +99,33 @@ public abstract class QueryBuilder {
|
|||||||
return execute(new QueryParams(type, repositoryId, queryString, start, limit));
|
return execute(new QueryParams(type, repositoryId, queryString, start, limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the query and returns the matches.
|
||||||
|
*
|
||||||
|
* @param typeName type name of objects which are searched
|
||||||
|
* @param queryString searched query
|
||||||
|
* @return result of query
|
||||||
|
*
|
||||||
|
* @throws NotFoundException if type could not be found
|
||||||
|
*/
|
||||||
|
public QueryResult execute(String typeName, String queryString){
|
||||||
|
Class<?> type = resolveByName(typeName).orElseThrow(() -> notFound(entity("type", typeName)));
|
||||||
|
return execute(type, queryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type by its name. Returns optional with class of type or an empty optional.
|
||||||
|
*
|
||||||
|
* @param typeName name of type
|
||||||
|
* @return optional with class of type or empty
|
||||||
|
*/
|
||||||
|
protected abstract Optional<Class<?>> resolveByName(String typeName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the query and returns the matches.
|
||||||
|
* @param queryParams query parameter
|
||||||
|
* @return result of query
|
||||||
|
*/
|
||||||
protected abstract QueryResult execute(QueryParams queryParams);
|
protected abstract QueryResult execute(QueryParams queryParams);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,14 +29,18 @@ import { createQueryString } from "./utils";
|
|||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
|
|
||||||
export type SearchOptions = {
|
export type SearchOptions = {
|
||||||
|
type: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultSearchOptions: SearchOptions = {};
|
const defaultSearchOptions: SearchOptions = {
|
||||||
|
type: "repository",
|
||||||
|
};
|
||||||
|
|
||||||
export const useSearch = (query: string, options = defaultSearchOptions): ApiResult<QueryResult> => {
|
export const useSearch = (query: string, optionParam = defaultSearchOptions): ApiResult<QueryResult> => {
|
||||||
const link = useRequiredIndexLink("search");
|
const options = { ...defaultSearchOptions, ...optionParam };
|
||||||
|
const link = useRequiredIndexLink("search").replace("{type}", options.type);
|
||||||
|
|
||||||
const queryParams: Record<string, string> = {};
|
const queryParams: Record<string, string> = {};
|
||||||
queryParams.q = query;
|
queryParams.q = query;
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
|||||||
builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self()));
|
builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self()));
|
||||||
builder.single(link("importLog", resourceLinks.repository().importLog("IMPORT_LOG_ID").replace("IMPORT_LOG_ID", "{logId}")));
|
builder.single(link("importLog", resourceLinks.repository().importLog("IMPORT_LOG_ID").replace("IMPORT_LOG_ID", "{logId}")));
|
||||||
|
|
||||||
builder.single(link("search", resourceLinks.search().search()));
|
builder.single(link("search", resourceLinks.search().search("INDEXED_TYPE").replace("INDEXED_TYPE", "{type}")));
|
||||||
} else {
|
} else {
|
||||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1125,8 +1125,8 @@ class ResourceLinks {
|
|||||||
this.searchLinkBuilder = new LinkBuilder(pathInfo, SearchResource.class);
|
this.searchLinkBuilder = new LinkBuilder(pathInfo, SearchResource.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String search() {
|
public String search(String type) {
|
||||||
return searchLinkBuilder.method("search").parameters().href();
|
return searchLinkBuilder.method("search").parameters(type).href();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import javax.validation.constraints.Max;
|
|||||||
import javax.validation.constraints.Min;
|
import javax.validation.constraints.Min;
|
||||||
import javax.validation.constraints.Size;
|
import javax.validation.constraints.Size;
|
||||||
import javax.ws.rs.DefaultValue;
|
import javax.ws.rs.DefaultValue;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
@@ -55,6 +56,9 @@ public class SearchParameters {
|
|||||||
@DefaultValue("10")
|
@DefaultValue("10")
|
||||||
private int pageSize = 10;
|
private int pageSize = 10;
|
||||||
|
|
||||||
|
@PathParam("type")
|
||||||
|
private String type;
|
||||||
|
|
||||||
String getSelfLink() {
|
String getSelfLink() {
|
||||||
return uriInfo.getAbsolutePath().toASCIIString();
|
return uriInfo.getAbsolutePath().toASCIIString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import sonia.scm.repository.Repository;
|
|
||||||
import sonia.scm.search.IndexNames;
|
import sonia.scm.search.IndexNames;
|
||||||
import sonia.scm.search.QueryResult;
|
import sonia.scm.search.QueryResult;
|
||||||
import sonia.scm.search.SearchEngine;
|
import sonia.scm.search.SearchEngine;
|
||||||
@@ -62,7 +61,7 @@ public class SearchResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("")
|
@Path("{type}")
|
||||||
@Produces(VndMediaType.QUERY_RESULT)
|
@Produces(VndMediaType.QUERY_RESULT)
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Query result",
|
summary = "Query result",
|
||||||
@@ -103,7 +102,7 @@ public class SearchResource {
|
|||||||
QueryResult result = engine.search(IndexNames.DEFAULT)
|
QueryResult result = engine.search(IndexNames.DEFAULT)
|
||||||
.start(params.getPage() * params.getPageSize())
|
.start(params.getPage() * params.getPageSize())
|
||||||
.limit(params.getPageSize())
|
.limit(params.getPageSize())
|
||||||
.execute(Repository.class, params.getQuery());
|
.execute(params.getType(), params.getQuery());
|
||||||
|
|
||||||
return mapper.map(params, result);
|
return mapper.map(params, result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,11 @@ public class DefaultExtensionProcessor implements ExtensionProcessor
|
|||||||
return collector.getWebElements();
|
return collector.getWebElements();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterable<Class<?>> getIndexedTypes() {
|
||||||
|
return collector.getIndexedTypes();
|
||||||
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
|
|||||||
@@ -223,6 +223,10 @@ public final class ExtensionCollector
|
|||||||
return webElements;
|
return webElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<Class<?>> getIndexedTypes() {
|
||||||
|
return indexedTypes;
|
||||||
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -255,7 +259,7 @@ public final class ExtensionCollector
|
|||||||
private void collectExtensions(ClassLoader defaultClassLoader, ScmModule module) {
|
private void collectExtensions(ClassLoader defaultClassLoader, ScmModule module) {
|
||||||
for (ClassElement extension : module.getExtensions()) {
|
for (ClassElement extension : module.getExtensions()) {
|
||||||
if (isRequirementFulfilled(extension)) {
|
if (isRequirementFulfilled(extension)) {
|
||||||
Class<?> extensionClass = loadExtension(defaultClassLoader, extension);
|
Class<?> extensionClass = load(defaultClassLoader, extension);
|
||||||
appendExtension(extensionClass);
|
appendExtension(extensionClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,25 +269,34 @@ public final class ExtensionCollector
|
|||||||
Set<Class<?>> classes = new HashSet<>();
|
Set<Class<?>> classes = new HashSet<>();
|
||||||
for (ClassElement element : classElements) {
|
for (ClassElement element : classElements) {
|
||||||
if (isRequirementFulfilled(element)) {
|
if (isRequirementFulfilled(element)) {
|
||||||
Class<?> loadedClass = loadExtension(defaultClassLoader, element);
|
Class<?> loadedClass = load(defaultClassLoader, element);
|
||||||
classes.add(loadedClass);
|
classes.add(loadedClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return classes;
|
return classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Collection<Class<?>> collectIndexedTypes(ClassLoader defaultClassLoader, Iterable<ClassElement> descriptors) {
|
||||||
|
Set<Class<?>> types = new HashSet<>();
|
||||||
|
for (ClassElement descriptor : descriptors) {
|
||||||
|
Class<?> loadedClass = load(defaultClassLoader, descriptor);
|
||||||
|
types.add(loadedClass);
|
||||||
|
}
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
private Set<WebElementExtension> collectWebElementExtensions(ClassLoader defaultClassLoader, Iterable<WebElementDescriptor> descriptors) {
|
private Set<WebElementExtension> collectWebElementExtensions(ClassLoader defaultClassLoader, Iterable<WebElementDescriptor> descriptors) {
|
||||||
Set<WebElementExtension> webElementExtensions = new HashSet<>();
|
Set<WebElementExtension> webElementExtensions = new HashSet<>();
|
||||||
for (WebElementDescriptor descriptor : descriptors) {
|
for (WebElementDescriptor descriptor : descriptors) {
|
||||||
if (isRequirementFulfilled(descriptor)) {
|
if (isRequirementFulfilled(descriptor)) {
|
||||||
Class<?> loadedClass = loadExtension(defaultClassLoader, descriptor);
|
Class<?> loadedClass = load(defaultClassLoader, descriptor);
|
||||||
webElementExtensions.add(new WebElementExtension(loadedClass, descriptor));
|
webElementExtensions.add(new WebElementExtension(loadedClass, descriptor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return webElementExtensions;
|
return webElementExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Class<?> loadExtension(ClassLoader classLoader, ClassElement extension) {
|
private Class<?> load(ClassLoader classLoader, ClassElement extension) {
|
||||||
try {
|
try {
|
||||||
return classLoader.loadClass(extension.getClazz());
|
return classLoader.loadClass(extension.getClazz());
|
||||||
} catch (ClassNotFoundException ex) {
|
} catch (ClassNotFoundException ex) {
|
||||||
@@ -320,6 +333,7 @@ public final class ExtensionCollector
|
|||||||
restResources.addAll(collectClasses(classLoader, module.getRestResources()));
|
restResources.addAll(collectClasses(classLoader, module.getRestResources()));
|
||||||
|
|
||||||
webElements.addAll(collectWebElementExtensions(classLoader, module.getWebElements()));
|
webElements.addAll(collectWebElementExtensions(classLoader, module.getWebElements()));
|
||||||
|
indexedTypes.addAll(collectIndexedTypes(classLoader, module.getIndexedTypes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
@@ -327,6 +341,8 @@ public final class ExtensionCollector
|
|||||||
/** Field description */
|
/** Field description */
|
||||||
private final Set<WebElementExtension> webElements = Sets.newHashSet();
|
private final Set<WebElementExtension> webElements = Sets.newHashSet();
|
||||||
|
|
||||||
|
private final Set<Class<?>> indexedTypes = Sets.newHashSet();
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private final Set<Class> restResources = Sets.newHashSet();
|
private final Set<Class> restResources = Sets.newHashSet();
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class IndexUpdateListener implements ServletContextListener {
|
|||||||
private static final Logger LOG = LoggerFactory.getLogger(IndexUpdateListener.class);
|
private static final Logger LOG = LoggerFactory.getLogger(IndexUpdateListener.class);
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final int INDEX_VERSION = 1;
|
static final int INDEX_VERSION = 2;
|
||||||
|
|
||||||
private final AdministrationContext administrationContext;
|
private final AdministrationContext administrationContext;
|
||||||
private final IndexQueue queue;
|
private final IndexQueue queue;
|
||||||
@@ -87,12 +87,22 @@ public class IndexUpdateListener implements ServletContextListener {
|
|||||||
@Override
|
@Override
|
||||||
public void contextInitialized(ServletContextEvent servletContextEvent) {
|
public void contextInitialized(ServletContextEvent servletContextEvent) {
|
||||||
Optional<IndexLog> indexLog = indexLogStore.get(IndexNames.DEFAULT, Repository.class);
|
Optional<IndexLog> indexLog = indexLogStore.get(IndexNames.DEFAULT, Repository.class);
|
||||||
if (!indexLog.isPresent()) {
|
if (indexLog.isPresent()) {
|
||||||
|
int version = indexLog.get().getVersion();
|
||||||
|
if (version < INDEX_VERSION) {
|
||||||
|
LOG.debug("repository index {} is older then {}, start reindexing of all repositories", version, INDEX_VERSION);
|
||||||
|
indexAll();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
LOG.debug("could not find log entry for repository index, start reindexing of all repositories");
|
LOG.debug("could not find log entry for repository index, start reindexing of all repositories");
|
||||||
|
indexAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void indexAll() {
|
||||||
administrationContext.runAsAdmin(ReIndexAll.class);
|
administrationContext.runAsAdmin(ReIndexAll.class);
|
||||||
indexLogStore.log(IndexNames.DEFAULT, Repository.class, INDEX_VERSION);
|
indexLogStore.log(IndexNames.DEFAULT, Repository.class, INDEX_VERSION);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void contextDestroyed(ServletContextEvent servletContextEvent) {
|
public void contextDestroyed(ServletContextEvent servletContextEvent) {
|
||||||
@@ -117,6 +127,8 @@ public class IndexUpdateListener implements ServletContextListener {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try (Index index = queue.getQueuedIndex(IndexNames.DEFAULT)) {
|
try (Index index = queue.getQueuedIndex(IndexNames.DEFAULT)) {
|
||||||
|
// delete v1 types
|
||||||
|
index.deleteByTypeName(Repository.class.getName());
|
||||||
for (Repository repository : repositoryManager.getAll()) {
|
for (Repository repository : repositoryManager.getAll()) {
|
||||||
store(index, repository);
|
store(index, repository);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,144 +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 com.google.common.base.Strings;
|
|
||||||
import org.apache.lucene.document.Document;
|
|
||||||
import org.apache.lucene.index.IndexableField;
|
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
import static java.util.Collections.emptySet;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class DocumentConverter {
|
|
||||||
|
|
||||||
private final Map<Class<?>, TypeConverter> typeConverter = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
Document convert(Object object) {
|
|
||||||
TypeConverter converter = typeConverter.computeIfAbsent(object.getClass(), this::createTypeConverter);
|
|
||||||
try {
|
|
||||||
return converter.convert(object);
|
|
||||||
} catch (IllegalAccessException | InvocationTargetException ex) {
|
|
||||||
throw new SearchEngineException("failed to create document", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TypeConverter createTypeConverter(Class<?> type) {
|
|
||||||
List<FieldConverter> fieldConverters = new ArrayList<>();
|
|
||||||
collectFields(fieldConverters, type);
|
|
||||||
return new TypeConverter(fieldConverters);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void collectFields(List<FieldConverter> fieldConverters, Class<?> type) {
|
|
||||||
Class<?> parent = type.getSuperclass();
|
|
||||||
if (parent != null) {
|
|
||||||
collectFields(fieldConverters, parent);
|
|
||||||
}
|
|
||||||
for (Field field : type.getDeclaredFields()) {
|
|
||||||
Indexed indexed = field.getAnnotation(Indexed.class);
|
|
||||||
if (indexed != null) {
|
|
||||||
IndexableFieldFactory fieldFactory = IndexableFields.create(field, indexed);
|
|
||||||
Method getter = findGetter(type, field);
|
|
||||||
fieldConverters.add(new FieldConverter(field, getter, indexed, fieldFactory));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Method findGetter(Class<?> type, Field field) {
|
|
||||||
String name = createGetterName(field);
|
|
||||||
try {
|
|
||||||
return type.getMethod(name);
|
|
||||||
} catch (NoSuchMethodException ex) {
|
|
||||||
throw new NonReadableFieldException("could not find getter for field", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String createGetterName(Field field) {
|
|
||||||
String fieldName = field.getName();
|
|
||||||
String prefix = "get";
|
|
||||||
if (field.getType() == Boolean.TYPE) {
|
|
||||||
prefix = "is";
|
|
||||||
}
|
|
||||||
return prefix + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class TypeConverter {
|
|
||||||
|
|
||||||
private final List<FieldConverter> fieldConverters;
|
|
||||||
|
|
||||||
private TypeConverter(List<FieldConverter> fieldConverters) {
|
|
||||||
this.fieldConverters = fieldConverters;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Document convert(Object object) throws IllegalAccessException, InvocationTargetException {
|
|
||||||
Document document = new Document();
|
|
||||||
for (FieldConverter fieldConverter : fieldConverters) {
|
|
||||||
for (IndexableField field : fieldConverter.convert(object)) {
|
|
||||||
document.add(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class FieldConverter {
|
|
||||||
|
|
||||||
private final Method getter;
|
|
||||||
private final IndexableFieldFactory fieldFactory;
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
private FieldConverter(Field field, Method getter, Indexed indexed, IndexableFieldFactory fieldFactory) {
|
|
||||||
this.getter = getter;
|
|
||||||
this.fieldFactory = fieldFactory;
|
|
||||||
this.name = createName(field, indexed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String createName(Field field, Indexed indexed) {
|
|
||||||
String nameFromAnnotation = indexed.name();
|
|
||||||
if (Strings.isNullOrEmpty(nameFromAnnotation)) {
|
|
||||||
return field.getName();
|
|
||||||
}
|
|
||||||
return nameFromAnnotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<IndexableField> convert(Object object) throws IllegalAccessException, InvocationTargetException {
|
|
||||||
Object value = getter.invoke(object);
|
|
||||||
if (value != null) {
|
|
||||||
return fieldFactory.create(name, value);
|
|
||||||
}
|
|
||||||
return emptySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* 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.base.Strings;
|
||||||
|
import org.apache.lucene.index.IndexableField;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptySet;
|
||||||
|
|
||||||
|
final class FieldConverter {
|
||||||
|
|
||||||
|
private final Method getter;
|
||||||
|
private final IndexableFieldFactory fieldFactory;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
FieldConverter(Field field, Method getter, Indexed indexed, IndexableFieldFactory fieldFactory) {
|
||||||
|
this.getter = getter;
|
||||||
|
this.fieldFactory = fieldFactory;
|
||||||
|
this.name = createName(field, indexed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createName(Field field, Indexed indexed) {
|
||||||
|
String nameFromAnnotation = indexed.name();
|
||||||
|
if (Strings.isNullOrEmpty(nameFromAnnotation)) {
|
||||||
|
return field.getName();
|
||||||
|
}
|
||||||
|
return nameFromAnnotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<IndexableField> convert(Object object) throws IllegalAccessException, InvocationTargetException {
|
||||||
|
Object value = getter.invoke(object);
|
||||||
|
if (value != null) {
|
||||||
|
return fieldFactory.create(name, value);
|
||||||
|
}
|
||||||
|
return emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -33,27 +33,32 @@ import org.apache.lucene.index.Term;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import static sonia.scm.search.FieldNames.*;
|
import static sonia.scm.search.FieldNames.ID;
|
||||||
|
import static sonia.scm.search.FieldNames.PERMISSION;
|
||||||
|
import static sonia.scm.search.FieldNames.REPOSITORY;
|
||||||
|
import static sonia.scm.search.FieldNames.TYPE;
|
||||||
|
import static sonia.scm.search.FieldNames.UID;
|
||||||
|
|
||||||
public class LuceneIndex implements Index {
|
public class LuceneIndex implements Index {
|
||||||
|
|
||||||
private final DocumentConverter converter;
|
private final SearchableTypeResolver resolver;
|
||||||
private final IndexWriter writer;
|
private final IndexWriter writer;
|
||||||
|
|
||||||
LuceneIndex(DocumentConverter converter, IndexWriter writer) {
|
LuceneIndex(SearchableTypeResolver resolver, IndexWriter writer) {
|
||||||
this.converter = converter;
|
this.resolver = resolver;
|
||||||
this.writer = writer;
|
this.writer = writer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void store(Id id, String permission, Object object) {
|
public void store(Id id, String permission, Object object) {
|
||||||
String uid = createUid(id, object.getClass());
|
SearchableType type = resolver.resolve(object);
|
||||||
Document document = converter.convert(object);
|
String uid = createUid(id, type);
|
||||||
|
Document document = type.getTypeConverter().convert(object);
|
||||||
try {
|
try {
|
||||||
field(document, UID, uid);
|
field(document, UID, uid);
|
||||||
field(document, ID, id.getValue());
|
field(document, ID, id.getValue());
|
||||||
id.getRepository().ifPresent(repository -> field(document, REPOSITORY, repository));
|
id.getRepository().ifPresent(repository -> field(document, REPOSITORY, repository));
|
||||||
field(document, TYPE, object.getClass().getName());
|
field(document, TYPE, type.getName());
|
||||||
if (!Strings.isNullOrEmpty(permission)) {
|
if (!Strings.isNullOrEmpty(permission)) {
|
||||||
field(document, PERMISSION, permission);
|
field(document, PERMISSION, permission);
|
||||||
}
|
}
|
||||||
@@ -63,7 +68,7 @@ public class LuceneIndex implements Index {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createUid(Id id, Class<?> type) {
|
private String createUid(Id id, SearchableType type) {
|
||||||
return id.asString() + "/" + type.getName();
|
return id.asString() + "/" + type.getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +78,9 @@ public class LuceneIndex implements Index {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(Id id, Class<?> type) {
|
public void delete(Id id, Class<?> type) {
|
||||||
|
SearchableType searchableType = resolver.resolve(type);
|
||||||
try {
|
try {
|
||||||
writer.deleteDocuments(new Term(UID, createUid(id, type)));
|
writer.deleteDocuments(new Term(UID, createUid(id, searchableType)));
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
@@ -91,10 +97,16 @@ public class LuceneIndex implements Index {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteByType(Class<?> type) {
|
public void deleteByType(Class<?> type) {
|
||||||
|
SearchableType searchableType = resolver.resolve(type);
|
||||||
|
deleteByTypeName(searchableType.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteByTypeName(String typeName) {
|
||||||
try {
|
try {
|
||||||
writer.deleteDocuments(new Term(TYPE, type.getName()));
|
writer.deleteDocuments(new Term(TYPE, typeName));
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new SearchEngineException("failed to delete documents by repository " + type + " from index", ex);
|
throw new SearchEngineException("failed to delete documents by repository " + typeName + " from index", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.search;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class LuceneIndexFactory {
|
||||||
|
|
||||||
|
private final SearchableTypeResolver typeResolver;
|
||||||
|
private final IndexOpener indexOpener;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public LuceneIndexFactory(SearchableTypeResolver typeResolver, IndexOpener indexOpener) {
|
||||||
|
this.typeResolver = typeResolver;
|
||||||
|
this.indexOpener = indexOpener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LuceneIndex create(String name, IndexOptions options) {
|
||||||
|
try {
|
||||||
|
return new LuceneIndex(typeResolver, indexOpener.openForWrite(name, options));
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new SearchEngineException("failed to open index " + name, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,32 +46,37 @@ import org.slf4j.LoggerFactory;
|
|||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
public class LuceneQueryBuilder extends QueryBuilder {
|
public class LuceneQueryBuilder extends QueryBuilder {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(LuceneQueryBuilder.class);
|
private static final Logger LOG = LoggerFactory.getLogger(LuceneQueryBuilder.class);
|
||||||
|
|
||||||
private static final Map<Class<?>, SearchableType> CACHE = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private final IndexOpener opener;
|
private final IndexOpener opener;
|
||||||
|
private final SearchableTypeResolver resolver;
|
||||||
private final String indexName;
|
private final String indexName;
|
||||||
private final Analyzer analyzer;
|
private final Analyzer analyzer;
|
||||||
|
|
||||||
LuceneQueryBuilder(IndexOpener opener, String indexName, Analyzer analyzer) {
|
LuceneQueryBuilder(IndexOpener opener, SearchableTypeResolver resolver, String indexName, Analyzer analyzer) {
|
||||||
this.opener = opener;
|
this.opener = opener;
|
||||||
|
this.resolver = resolver;
|
||||||
this.indexName = indexName;
|
this.indexName = indexName;
|
||||||
this.analyzer = analyzer;
|
this.analyzer = analyzer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Optional<Class<?>> resolveByName(String typeName) {
|
||||||
|
return resolver.resolveClassByName(typeName);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected QueryResult execute(QueryParams queryParams) {
|
protected QueryResult execute(QueryParams queryParams) {
|
||||||
String queryString = Strings.nullToEmpty(queryParams.getQueryString());
|
String queryString = Strings.nullToEmpty(queryParams.getQueryString());
|
||||||
|
|
||||||
SearchableType searchableType = CACHE.computeIfAbsent(queryParams.getType(), SearchableTypes::create);
|
SearchableType searchableType = resolver.resolve(queryParams.getType());
|
||||||
|
|
||||||
Query query = Queries.filter(createQuery(searchableType, queryParams, queryString), queryParams);
|
Query parsedQuery = createQuery(searchableType, queryParams, queryString);
|
||||||
|
Query query = Queries.filter(parsedQuery, searchableType, queryParams);
|
||||||
if (LOG.isDebugEnabled()) {
|
if (LOG.isDebugEnabled()) {
|
||||||
LOG.debug("execute lucene query: {}", query);
|
LOG.debug("execute lucene query: {}", query);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,16 +29,18 @@ import javax.inject.Inject;
|
|||||||
public class LuceneQueryBuilderFactory {
|
public class LuceneQueryBuilderFactory {
|
||||||
|
|
||||||
private final IndexOpener indexOpener;
|
private final IndexOpener indexOpener;
|
||||||
|
private final SearchableTypeResolver searchableTypeResolver;
|
||||||
private final AnalyzerFactory analyzerFactory;
|
private final AnalyzerFactory analyzerFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public LuceneQueryBuilderFactory(IndexOpener indexOpener, AnalyzerFactory analyzerFactory) {
|
public LuceneQueryBuilderFactory(IndexOpener indexOpener, SearchableTypeResolver searchableTypeResolver, AnalyzerFactory analyzerFactory) {
|
||||||
this.indexOpener = indexOpener;
|
this.indexOpener = indexOpener;
|
||||||
|
this.searchableTypeResolver = searchableTypeResolver;
|
||||||
this.analyzerFactory = analyzerFactory;
|
this.analyzerFactory = analyzerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LuceneQueryBuilder create(String name, IndexOptions options) {
|
public LuceneQueryBuilder create(String name, IndexOptions options) {
|
||||||
return new LuceneQueryBuilder(indexOpener, name, analyzerFactory.create(options));
|
return new LuceneQueryBuilder(indexOpener, searchableTypeResolver, name, analyzerFactory.create(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,28 +25,21 @@
|
|||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class LuceneSearchEngine implements SearchEngine {
|
public class LuceneSearchEngine implements SearchEngine {
|
||||||
|
|
||||||
private final IndexOpener indexOpener;
|
private final LuceneIndexFactory indexFactory;
|
||||||
private final DocumentConverter converter;
|
|
||||||
private final LuceneQueryBuilderFactory queryBuilderFactory;
|
private final LuceneQueryBuilderFactory queryBuilderFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public LuceneSearchEngine(IndexOpener indexOpener, DocumentConverter converter, LuceneQueryBuilderFactory queryBuilderFactory) {
|
public LuceneSearchEngine(LuceneIndexFactory indexFactory, LuceneQueryBuilderFactory queryBuilderFactory) {
|
||||||
this.indexOpener = indexOpener;
|
this.indexFactory = indexFactory;
|
||||||
this.converter = converter;
|
|
||||||
this.queryBuilderFactory = queryBuilderFactory;
|
this.queryBuilderFactory = queryBuilderFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Index getOrCreate(String name, IndexOptions options) {
|
public Index getOrCreate(String name, IndexOptions options) {
|
||||||
try {
|
return indexFactory.create(name, options);
|
||||||
return new LuceneIndex(converter, indexOpener.openForWrite(name, options));
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new SearchEngineException("failed to open index", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ final class Queries {
|
|||||||
private Queries() {
|
private Queries() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Query typeQuery(Class<?> type) {
|
private static Query typeQuery(SearchableType type) {
|
||||||
return new TermQuery(new Term(FieldNames.TYPE, type.getName()));
|
return new TermQuery(new Term(FieldNames.TYPE, type.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,10 +44,10 @@ final class Queries {
|
|||||||
return new TermQuery(new Term(FieldNames.REPOSITORY, repositoryId));
|
return new TermQuery(new Term(FieldNames.REPOSITORY, repositoryId));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Query filter(Query query, QueryBuilder.QueryParams params) {
|
static Query filter(Query query, SearchableType searchableType, QueryBuilder.QueryParams params) {
|
||||||
BooleanQuery.Builder builder = new BooleanQuery.Builder()
|
BooleanQuery.Builder builder = new BooleanQuery.Builder()
|
||||||
.add(query, MUST)
|
.add(query, MUST)
|
||||||
.add(typeQuery(params.getType()), MUST);
|
.add(typeQuery(searchableType), MUST);
|
||||||
params.getRepositoryId().ifPresent(repo -> builder.add(repositoryQuery(repo), MUST));
|
params.getRepositoryId().ifPresent(repo -> builder.add(repositoryQuery(repo), MUST));
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ public class QueuedIndex implements Index {
|
|||||||
tasks.add(index -> index.deleteByType(type));
|
tasks.add(index -> index.deleteByType(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteByTypeName(String typeName) {
|
||||||
|
tasks.add(index -> index.deleteByTypeName(typeName));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
IndexQueueTaskWrapper wrappedTask = new IndexQueueTaskWrapper(
|
IndexQueueTaskWrapper wrappedTask = new IndexQueueTaskWrapper(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
package sonia.scm.search;
|
package sonia.scm.search;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
|
import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
|
||||||
|
|
||||||
@@ -35,17 +36,40 @@ import java.util.Map;
|
|||||||
public class SearchableType {
|
public class SearchableType {
|
||||||
|
|
||||||
Class<?> type;
|
Class<?> type;
|
||||||
|
String name;
|
||||||
String[] fieldNames;
|
String[] fieldNames;
|
||||||
Map<String,Float> boosts;
|
Map<String,Float> boosts;
|
||||||
Map<String, PointsConfig> pointsConfig;
|
Map<String, PointsConfig> pointsConfig;
|
||||||
List<SearchableField> fields;
|
List<SearchableField> fields;
|
||||||
|
TypeConverter typeConverter;
|
||||||
|
|
||||||
SearchableType(Class<?> type, String[] fieldNames, Map<String, Float> boosts, Map<String, PointsConfig> pointsConfig, List<SearchableField> fields) {
|
SearchableType(Class<?> type,
|
||||||
|
String[] fieldNames,
|
||||||
|
Map<String, Float> boosts,
|
||||||
|
Map<String, PointsConfig> pointsConfig,
|
||||||
|
List<SearchableField> fields,
|
||||||
|
TypeConverter typeConverter) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
this.name = name(type);
|
||||||
this.fieldNames = fieldNames;
|
this.fieldNames = fieldNames;
|
||||||
this.boosts = Collections.unmodifiableMap(boosts);
|
this.boosts = Collections.unmodifiableMap(boosts);
|
||||||
this.pointsConfig = Collections.unmodifiableMap(pointsConfig);
|
this.pointsConfig = Collections.unmodifiableMap(pointsConfig);
|
||||||
this.fields = Collections.unmodifiableList(fields);
|
this.fields = Collections.unmodifiableList(fields);
|
||||||
|
this.typeConverter = typeConverter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String name(Class<?> type) {
|
||||||
|
IndexedType annotation = type.getAnnotation(IndexedType.class);
|
||||||
|
if (annotation == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
type.getName() + " has no " + IndexedType.class.getSimpleName() + " annotation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
String nameFromAnnotation = annotation.value();
|
||||||
|
if (Strings.isNullOrEmpty(nameFromAnnotation)) {
|
||||||
|
String simpleName = type.getSimpleName();
|
||||||
|
return Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1);
|
||||||
|
}
|
||||||
|
return nameFromAnnotation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* 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 sonia.scm.plugin.PluginLoader;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
import static sonia.scm.NotFoundException.notFound;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class SearchableTypeResolver {
|
||||||
|
|
||||||
|
private final Map<Class<?>, SearchableType> classToSearchableType = new HashMap<>();
|
||||||
|
private final Map<String, Class<?>> nameToClass = new HashMap<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SearchableTypeResolver(PluginLoader pluginLoader) {
|
||||||
|
this(pluginLoader.getExtensionProcessor().getIndexedTypes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
SearchableTypeResolver(Class<?>... indexedTypes) {
|
||||||
|
this(Arrays.asList(indexedTypes));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
SearchableTypeResolver(Iterable<Class<?>> indexedTypes) {
|
||||||
|
fillMaps(convert(indexedTypes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillMaps(Iterable<SearchableType> types) {
|
||||||
|
for (SearchableType type : types) {
|
||||||
|
classToSearchableType.put(type.getType(), type);
|
||||||
|
nameToClass.put(type.getName(), type.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Set<SearchableType> convert(Iterable<Class<?>> indexedTypes) {
|
||||||
|
return StreamSupport.stream(indexedTypes.spliterator(), false)
|
||||||
|
.map(SearchableTypes::create)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchableType resolve(Object object) {
|
||||||
|
return resolve(object.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchableType resolve(Class<?> type) {
|
||||||
|
SearchableType searchableType = classToSearchableType.get(type);
|
||||||
|
if (searchableType == null) {
|
||||||
|
throw notFound(entity("type", type.getName()));
|
||||||
|
}
|
||||||
|
return searchableType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Class<?>> resolveClassByName(String typeName) {
|
||||||
|
return Optional.ofNullable(nameToClass.get(typeName));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ final class SearchableTypes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SearchableType(type, fieldsNames, boosts, pointsConfig, fields);
|
return new SearchableType(type, fieldsNames, boosts, pointsConfig, fields, TypeConverters.create(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void collectFields(Class<?> type, List<SearchableField> fields) {
|
private static void collectFields(Class<?> type, List<SearchableField> fields) {
|
||||||
|
|||||||
61
scm-webapp/src/main/java/sonia/scm/search/TypeConverter.java
Normal file
61
scm-webapp/src/main/java/sonia/scm/search/TypeConverter.java
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.IndexableField;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class TypeConverter {
|
||||||
|
|
||||||
|
private final List<FieldConverter> fieldConverters;
|
||||||
|
|
||||||
|
TypeConverter(List<FieldConverter> fieldConverters) {
|
||||||
|
this.fieldConverters = fieldConverters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Document convert(Object object) {
|
||||||
|
try {
|
||||||
|
return doConversion(object);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException ex) {
|
||||||
|
throw new SearchEngineException("failed to create document", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Document doConversion(Object object) throws IllegalAccessException, InvocationTargetException {
|
||||||
|
Document document = new Document();
|
||||||
|
for (FieldConverter fieldConverter : fieldConverters) {
|
||||||
|
for (IndexableField field : fieldConverter.convert(object)) {
|
||||||
|
document.add(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* 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.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
final class TypeConverters {
|
||||||
|
|
||||||
|
private TypeConverters() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static TypeConverter create(Class<?> type) {
|
||||||
|
List<FieldConverter> fieldConverters = new ArrayList<>();
|
||||||
|
collectFields(fieldConverters, type);
|
||||||
|
return new TypeConverter(fieldConverters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectFields(List<FieldConverter> fieldConverters, Class<?> type) {
|
||||||
|
Class<?> parent = type.getSuperclass();
|
||||||
|
if (parent != null) {
|
||||||
|
collectFields(fieldConverters, parent);
|
||||||
|
}
|
||||||
|
for (Field field : type.getDeclaredFields()) {
|
||||||
|
Indexed indexed = field.getAnnotation(Indexed.class);
|
||||||
|
if (indexed != null) {
|
||||||
|
IndexableFieldFactory fieldFactory = IndexableFields.create(field, indexed);
|
||||||
|
Method getter = findGetter(type, field);
|
||||||
|
fieldConverters.add(new FieldConverter(field, getter, indexed, fieldFactory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method findGetter(Class<?> type, Field field) {
|
||||||
|
String name = createGetterName(field);
|
||||||
|
try {
|
||||||
|
return type.getMethod(name);
|
||||||
|
} catch (NoSuchMethodException ex) {
|
||||||
|
throw new NonReadableFieldException("could not find getter for field", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createGetterName(Field field) {
|
||||||
|
String fieldName = field.getName();
|
||||||
|
String prefix = "get";
|
||||||
|
if (field.getType() == Boolean.TYPE) {
|
||||||
|
prefix = "is";
|
||||||
|
}
|
||||||
|
return prefix + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
class ContentTypeResolverTest {
|
class ContentSearchableTypeResolverTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldResolveMarkdown() {
|
void shouldResolveMarkdown() {
|
||||||
@@ -135,11 +135,11 @@ class SearchResourceTest {
|
|||||||
JsonMockHttpResponse response = search("paging", 1, 20);
|
JsonMockHttpResponse response = search("paging", 1, 20);
|
||||||
|
|
||||||
JsonNode links = response.getContentAsJson().get("_links");
|
JsonNode links = response.getContentAsJson().get("_links");
|
||||||
assertLink(links, "self", "/v2/search?q=paging&page=1&pageSize=20");
|
assertLink(links, "self", "/v2/search/string?q=paging&page=1&pageSize=20");
|
||||||
assertLink(links, "first", "/v2/search?q=paging&page=0&pageSize=20");
|
assertLink(links, "first", "/v2/search/string?q=paging&page=0&pageSize=20");
|
||||||
assertLink(links, "prev", "/v2/search?q=paging&page=0&pageSize=20");
|
assertLink(links, "prev", "/v2/search/string?q=paging&page=0&pageSize=20");
|
||||||
assertLink(links, "next", "/v2/search?q=paging&page=2&pageSize=20");
|
assertLink(links, "next", "/v2/search/string?q=paging&page=2&pageSize=20");
|
||||||
assertLink(links, "last", "/v2/search?q=paging&page=4&pageSize=20");
|
assertLink(links, "last", "/v2/search/string?q=paging&page=4&pageSize=20");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -220,7 +220,7 @@ class SearchResourceTest {
|
|||||||
searchEngine.search(IndexNames.DEFAULT)
|
searchEngine.search(IndexNames.DEFAULT)
|
||||||
.start(start)
|
.start(start)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.execute(Repository.class, query)
|
.execute("string", query)
|
||||||
).thenReturn(result);
|
).thenReturn(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ class SearchResourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private JsonMockHttpResponse search(String query, Integer page, Integer pageSize) throws URISyntaxException, UnsupportedEncodingException {
|
private JsonMockHttpResponse search(String query, Integer page, Integer pageSize) throws URISyntaxException, UnsupportedEncodingException {
|
||||||
String uri = "/v2/search?q=" + URLEncoder.encode(query, "UTF-8");
|
String uri = "/v2/search/string?q=" + URLEncoder.encode(query, "UTF-8");
|
||||||
if (page != null) {
|
if (page != null) {
|
||||||
uri += "&page=" + page;
|
uri += "&page=" + page;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,13 +61,16 @@ class DefaultIndexQueueTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
void createQueue() throws IOException {
|
void createQueue() throws IOException {
|
||||||
directory = new ByteBuffersDirectory();
|
directory = new ByteBuffersDirectory();
|
||||||
IndexOpener factory = mock(IndexOpener.class);
|
IndexOpener opener = mock(IndexOpener.class);
|
||||||
when(factory.openForWrite(any(String.class), any(IndexOptions.class))).thenAnswer(ic -> {
|
when(opener.openForWrite(any(String.class), any(IndexOptions.class))).thenAnswer(ic -> {
|
||||||
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);
|
return new IndexWriter(directory, config);
|
||||||
});
|
});
|
||||||
SearchEngine engine = new LuceneSearchEngine(factory, new DocumentConverter(), queryBuilderFactory);
|
|
||||||
|
SearchableTypeResolver resolver = new SearchableTypeResolver(Account.class, IndexedNumber.class);
|
||||||
|
LuceneIndexFactory indexFactory = new LuceneIndexFactory(resolver, opener);
|
||||||
|
SearchEngine engine = new LuceneSearchEngine(indexFactory, queryBuilderFactory);
|
||||||
queue = new DefaultIndexQueue(engine);
|
queue = new DefaultIndexQueue(engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +114,7 @@ class DefaultIndexQueueTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
|
@IndexedType
|
||||||
public static class Account {
|
public static class Account {
|
||||||
@Indexed
|
@Indexed
|
||||||
String username;
|
String username;
|
||||||
@@ -121,6 +125,7 @@ class DefaultIndexQueueTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
|
@IndexedType
|
||||||
public static class IndexedNumber {
|
public static class IndexedNumber {
|
||||||
@Indexed
|
@Indexed
|
||||||
int value;
|
int value;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class LuceneIndexTest {
|
|||||||
index.store(ONE, null, new Storable("Awesome content which should be indexed"));
|
index.store(ONE, null, new Storable("Awesome content which should be indexed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
assertHits(UID, "one/" + Storable.class.getName(), 1);
|
assertHits(UID, "one/storable", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -109,7 +109,7 @@ class LuceneIndexTest {
|
|||||||
index.store(ONE, null, new Storable("Some other text"));
|
index.store(ONE, null, new Storable("Some other text"));
|
||||||
}
|
}
|
||||||
|
|
||||||
assertHits(TYPE, Storable.class.getName(), 1);
|
assertHits(TYPE, "storable", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -214,7 +214,8 @@ class LuceneIndexTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private LuceneIndex createIndex() throws IOException {
|
private LuceneIndex createIndex() throws IOException {
|
||||||
return new LuceneIndex(new DocumentConverter(), createWriter());
|
SearchableTypeResolver resolver = new SearchableTypeResolver(Storable.class, OtherStorable.class);
|
||||||
|
return new LuceneIndex(resolver, createWriter());
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndexWriter createWriter() throws IOException {
|
private IndexWriter createWriter() throws IOException {
|
||||||
@@ -224,12 +225,14 @@ class LuceneIndexTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
|
@IndexedType
|
||||||
private static class Storable {
|
private static class Storable {
|
||||||
@Indexed
|
@Indexed
|
||||||
String value;
|
String value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
|
@IndexedType
|
||||||
private static class OtherStorable {
|
private static class OtherStorable {
|
||||||
@Indexed
|
@Indexed
|
||||||
String value;
|
String value;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import lombok.Getter;
|
||||||
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||||
import org.apache.lucene.document.Document;
|
import org.apache.lucene.document.Document;
|
||||||
import org.apache.lucene.document.Field;
|
import org.apache.lucene.document.Field;
|
||||||
@@ -153,7 +154,7 @@ class LuceneQueryBuilderTest {
|
|||||||
try (IndexWriter writer = writer()) {
|
try (IndexWriter writer = writer()) {
|
||||||
writer.addDocument(personDoc("Dent"));
|
writer.addDocument(personDoc("Dent"));
|
||||||
}
|
}
|
||||||
assertThrows(QueryParseException.class, () -> query(String.class, ":~:~"));
|
assertThrows(QueryParseException.class, () -> query(InetOrgPerson.class, ":~:~"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -247,8 +248,9 @@ class LuceneQueryBuilderTest {
|
|||||||
QueryResult result;
|
QueryResult result;
|
||||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||||
when(opener.openForRead("default")).thenReturn(reader);
|
when(opener.openForRead("default")).thenReturn(reader);
|
||||||
|
SearchableTypeResolver resolver = new SearchableTypeResolver(Simple.class);
|
||||||
LuceneQueryBuilder builder = new LuceneQueryBuilder(
|
LuceneQueryBuilder builder = new LuceneQueryBuilder(
|
||||||
opener, "default", new StandardAnalyzer()
|
opener, resolver, "default", new StandardAnalyzer()
|
||||||
);
|
);
|
||||||
result = builder.repository("cde").execute(Simple.class, "content:awesome");
|
result = builder.repository("cde").execute(Simple.class, "content:awesome");
|
||||||
}
|
}
|
||||||
@@ -480,8 +482,9 @@ class LuceneQueryBuilderTest {
|
|||||||
private QueryResult query(Class<?> type, String queryString, Integer start, Integer limit) throws IOException {
|
private 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);
|
lenient().when(opener.openForRead("default")).thenReturn(reader);
|
||||||
|
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
||||||
LuceneQueryBuilder builder = new LuceneQueryBuilder(
|
LuceneQueryBuilder builder = new LuceneQueryBuilder(
|
||||||
opener, "default", new StandardAnalyzer()
|
opener, resolver, "default", new StandardAnalyzer()
|
||||||
);
|
);
|
||||||
if (start != null) {
|
if (start != null) {
|
||||||
builder.start(start);
|
builder.start(start);
|
||||||
@@ -502,14 +505,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.class.getName(), 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.class.getName(), 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;
|
||||||
}
|
}
|
||||||
@@ -517,7 +520,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.class.getName(), 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;
|
||||||
}
|
}
|
||||||
@@ -529,14 +532,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.class.getName(), 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.class.getName(), Field.Store.YES));
|
document.add(new StringField(FieldNames.TYPE, "person", Field.Store.YES));
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,10 +552,12 @@ 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.class.getName(), Field.Store.YES));
|
document.add(new StringField(FieldNames.TYPE, "types", Field.Store.YES));
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@IndexedType
|
||||||
static class Types {
|
static class Types {
|
||||||
|
|
||||||
@Indexed
|
@Indexed
|
||||||
@@ -566,12 +571,16 @@ class LuceneQueryBuilderTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@IndexedType
|
||||||
static class Person {
|
static class Person {
|
||||||
|
|
||||||
@Indexed(defaultQuery = true)
|
@Indexed(defaultQuery = true)
|
||||||
private String lastName;
|
private String lastName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@IndexedType
|
||||||
static class InetOrgPerson extends Person {
|
static class InetOrgPerson extends Person {
|
||||||
|
|
||||||
@Indexed(defaultQuery = true, boost = 2f)
|
@Indexed(defaultQuery = true, boost = 2f)
|
||||||
@@ -584,6 +593,8 @@ class LuceneQueryBuilderTest {
|
|||||||
private String carLicense;
|
private String carLicense;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@IndexedType
|
||||||
static class Simple {
|
static class Simple {
|
||||||
@Indexed(defaultQuery = true)
|
@Indexed(defaultQuery = true)
|
||||||
private String content;
|
private String content;
|
||||||
|
|||||||
@@ -35,62 +35,61 @@ import org.apache.lucene.index.IndexableFieldType;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
class DocumentConverterTest {
|
class TypeConvertersTest {
|
||||||
|
|
||||||
private DocumentConverter documentConverter;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void prepare() {
|
|
||||||
documentConverter = new DocumentConverter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldConvertPersonToDocument() {
|
void shouldConvertPersonToDocument() {
|
||||||
Person person = new Person("Arthur", "Dent");
|
Person person = new Person("Arthur", "Dent");
|
||||||
|
|
||||||
Document document = documentConverter.convert(person);
|
Document document = convert(person);
|
||||||
|
|
||||||
assertThat(document.getField("firstName").stringValue()).isEqualTo("Arthur");
|
assertThat(document.getField("firstName").stringValue()).isEqualTo("Arthur");
|
||||||
assertThat(document.getField("lastName").stringValue()).isEqualTo("Dent");
|
assertThat(document.getField("lastName").stringValue()).isEqualTo("Dent");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Document convert(Object object) {
|
||||||
|
return TypeConverters.create(object.getClass()).convert(object);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUseNameFromAnnotation() {
|
void shouldUseNameFromAnnotation() {
|
||||||
Document document = documentConverter.convert(new ParamSample());
|
Document document = convert(new ParamSample());
|
||||||
|
|
||||||
assertThat(document.getField("username").stringValue()).isEqualTo("dent");
|
assertThat(document.getField("username").stringValue()).isEqualTo("dent");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldBeIndexedAsTextFieldByDefault() {
|
void shouldBeIndexedAsTextFieldByDefault() {
|
||||||
Document document = documentConverter.convert(new ParamSample());
|
Document document = convert(new ParamSample());
|
||||||
|
|
||||||
assertThat(document.getField("username")).isInstanceOf(TextField.class);
|
assertThat(document.getField("username")).isInstanceOf(TextField.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldBeIndexedAsStringField() {
|
void shouldBeIndexedAsStringField() {
|
||||||
Document document = documentConverter.convert(new ParamSample());
|
Document document = convert(new ParamSample());
|
||||||
|
|
||||||
assertThat(document.getField("searchable")).isInstanceOf(StringField.class);
|
assertThat(document.getField("searchable")).isInstanceOf(StringField.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldBeIndexedAsStoredField() {
|
void shouldBeIndexedAsStoredField() {
|
||||||
Document document = documentConverter.convert(new ParamSample());
|
Document document = convert(new ParamSample());
|
||||||
|
|
||||||
assertThat(document.getField("storedOnly")).isInstanceOf(StoredField.class);
|
assertThat(document.getField("storedOnly")).isInstanceOf(StoredField.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldIgnoreNonIndexedFields() {
|
void shouldIgnoreNonIndexedFields() {
|
||||||
Document document = documentConverter.convert(new ParamSample());
|
Document document = convert(new ParamSample());
|
||||||
|
|
||||||
assertThat(document.getField("notIndexed")).isNull();
|
assertThat(document.getField("notIndexed")).isNull();
|
||||||
}
|
}
|
||||||
@@ -99,7 +98,7 @@ class DocumentConverterTest {
|
|||||||
void shouldSupportInheritance() {
|
void shouldSupportInheritance() {
|
||||||
Account account = new Account("Arthur", "Dent", "arthur@hitchhiker.com");
|
Account account = new Account("Arthur", "Dent", "arthur@hitchhiker.com");
|
||||||
|
|
||||||
Document document = documentConverter.convert(account);
|
Document document = convert(account);
|
||||||
|
|
||||||
assertThat(document.getField("firstName")).isNotNull();
|
assertThat(document.getField("firstName")).isNotNull();
|
||||||
assertThat(document.getField("lastName")).isNotNull();
|
assertThat(document.getField("lastName")).isNotNull();
|
||||||
@@ -109,18 +108,18 @@ class DocumentConverterTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldFailWithoutGetter() {
|
void shouldFailWithoutGetter() {
|
||||||
WithoutGetter withoutGetter = new WithoutGetter();
|
WithoutGetter withoutGetter = new WithoutGetter();
|
||||||
assertThrows(NonReadableFieldException.class, () -> documentConverter.convert(withoutGetter));
|
assertThrows(NonReadableFieldException.class, () -> convert(withoutGetter));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFailOnUnsupportedFieldType() {
|
void shouldFailOnUnsupportedFieldType() {
|
||||||
UnsupportedFieldType unsupportedFieldType = new UnsupportedFieldType();
|
UnsupportedFieldType unsupportedFieldType = new UnsupportedFieldType();
|
||||||
assertThrows(UnsupportedTypeOfFieldException.class, () -> documentConverter.convert(unsupportedFieldType));
|
assertThrows(UnsupportedTypeOfFieldException.class, () -> convert(unsupportedFieldType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldStoreLongFieldsAsPointAndStoredByDefault() {
|
void shouldStoreLongFieldsAsPointAndStoredByDefault() {
|
||||||
Document document = documentConverter.convert(new SupportedTypes());
|
Document document = convert(new SupportedTypes());
|
||||||
|
|
||||||
assertPointField(document, "longType",
|
assertPointField(document, "longType",
|
||||||
field -> assertThat(field.numericValue().longValue()).isEqualTo(42L)
|
field -> assertThat(field.numericValue().longValue()).isEqualTo(42L)
|
||||||
@@ -129,7 +128,7 @@ class DocumentConverterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldStoreLongFieldAsStored() {
|
void shouldStoreLongFieldAsStored() {
|
||||||
Document document = documentConverter.convert(new SupportedTypes());
|
Document document = convert(new SupportedTypes());
|
||||||
|
|
||||||
IndexableField field = document.getField("storedOnlyLongType");
|
IndexableField field = document.getField("storedOnlyLongType");
|
||||||
assertThat(field).isInstanceOf(StoredField.class);
|
assertThat(field).isInstanceOf(StoredField.class);
|
||||||
@@ -138,7 +137,7 @@ class DocumentConverterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldStoreIntegerFieldsAsPointAndStoredByDefault() {
|
void shouldStoreIntegerFieldsAsPointAndStoredByDefault() {
|
||||||
Document document = documentConverter.convert(new SupportedTypes());
|
Document document = convert(new SupportedTypes());
|
||||||
|
|
||||||
assertPointField(document, "intType",
|
assertPointField(document, "intType",
|
||||||
field -> assertThat(field.numericValue().intValue()).isEqualTo(42)
|
field -> assertThat(field.numericValue().intValue()).isEqualTo(42)
|
||||||
@@ -147,7 +146,7 @@ class DocumentConverterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldStoreIntegerFieldAsStored() {
|
void shouldStoreIntegerFieldAsStored() {
|
||||||
Document document = documentConverter.convert(new SupportedTypes());
|
Document document = convert(new SupportedTypes());
|
||||||
|
|
||||||
IndexableField field = document.getField("storedOnlyIntegerType");
|
IndexableField field = document.getField("storedOnlyIntegerType");
|
||||||
assertThat(field).isInstanceOf(StoredField.class);
|
assertThat(field).isInstanceOf(StoredField.class);
|
||||||
@@ -156,7 +155,7 @@ class DocumentConverterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldStoreBooleanFieldsAsStringField() {
|
void shouldStoreBooleanFieldsAsStringField() {
|
||||||
Document document = documentConverter.convert(new SupportedTypes());
|
Document document = convert(new SupportedTypes());
|
||||||
|
|
||||||
IndexableField field = document.getField("boolType");
|
IndexableField field = document.getField("boolType");
|
||||||
assertThat(field).isInstanceOf(StringField.class);
|
assertThat(field).isInstanceOf(StringField.class);
|
||||||
@@ -166,7 +165,7 @@ class DocumentConverterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldStoreBooleanFieldAsStored() {
|
void shouldStoreBooleanFieldAsStored() {
|
||||||
Document document = documentConverter.convert(new SupportedTypes());
|
Document document = convert(new SupportedTypes());
|
||||||
|
|
||||||
IndexableField field = document.getField("storedOnlyBoolType");
|
IndexableField field = document.getField("storedOnlyBoolType");
|
||||||
assertThat(field).isInstanceOf(StoredField.class);
|
assertThat(field).isInstanceOf(StoredField.class);
|
||||||
@@ -176,7 +175,7 @@ class DocumentConverterTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldStoreInstantFieldsAsPointAndStoredByDefault() {
|
void shouldStoreInstantFieldsAsPointAndStoredByDefault() {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
Document document = documentConverter.convert(new DateTypes(now));
|
Document document = convert(new DateTypes(now));
|
||||||
|
|
||||||
assertPointField(document, "instant",
|
assertPointField(document, "instant",
|
||||||
field -> assertThat(field.numericValue().longValue()).isEqualTo(now.toEpochMilli())
|
field -> assertThat(field.numericValue().longValue()).isEqualTo(now.toEpochMilli())
|
||||||
@@ -186,7 +185,7 @@ class DocumentConverterTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldStoreInstantFieldAsStored() {
|
void shouldStoreInstantFieldAsStored() {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
Document document = documentConverter.convert(new DateTypes(now));
|
Document document = convert(new DateTypes(now));
|
||||||
|
|
||||||
IndexableField field = document.getField("storedOnlyInstant");
|
IndexableField field = document.getField("storedOnlyInstant");
|
||||||
assertThat(field).isInstanceOf(StoredField.class);
|
assertThat(field).isInstanceOf(StoredField.class);
|
||||||
@@ -195,7 +194,7 @@ class DocumentConverterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCreateNoFieldForNullValues() {
|
void shouldCreateNoFieldForNullValues() {
|
||||||
Document document = documentConverter.convert(new Person("Trillian", null));
|
Document document = convert(new Person("Trillian", null));
|
||||||
|
|
||||||
assertThat(document.getField("firstName")).isNotNull();
|
assertThat(document.getField("firstName")).isNotNull();
|
||||||
assertThat(document.getField("lastName")).isNull();
|
assertThat(document.getField("lastName")).isNull();
|
||||||
@@ -210,6 +209,7 @@ class DocumentConverterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@IndexedType
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public static class Person {
|
public static class Person {
|
||||||
@Indexed
|
@Indexed
|
||||||
@@ -219,6 +219,7 @@ class DocumentConverterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@IndexedType
|
||||||
public static class Account extends Person {
|
public static class Account extends Person {
|
||||||
@Indexed
|
@Indexed
|
||||||
private String mail;
|
private String mail;
|
||||||
@@ -230,6 +231,7 @@ class DocumentConverterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@IndexedType
|
||||||
public static class ParamSample {
|
public static class ParamSample {
|
||||||
@Indexed(name = "username")
|
@Indexed(name = "username")
|
||||||
private final String name = "dent";
|
private final String name = "dent";
|
||||||
@@ -243,18 +245,21 @@ class DocumentConverterTest {
|
|||||||
private final String notIndexed = "--";
|
private final String notIndexed = "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IndexedType
|
||||||
public static class WithoutGetter {
|
public static class WithoutGetter {
|
||||||
@Indexed
|
@Indexed
|
||||||
private final String value = "one";
|
private final String value = "one";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@IndexedType
|
||||||
public static class UnsupportedFieldType {
|
public static class UnsupportedFieldType {
|
||||||
@Indexed
|
@Indexed
|
||||||
private final Object value = "one";
|
private final Object value = "one";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@IndexedType
|
||||||
public static class SupportedTypes {
|
public static class SupportedTypes {
|
||||||
@Indexed
|
@Indexed
|
||||||
private final Long longType = 42L;
|
private final Long longType = 42L;
|
||||||
@@ -273,6 +278,7 @@ class DocumentConverterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@IndexedType
|
||||||
private static class DateTypes {
|
private static class DateTypes {
|
||||||
@Indexed
|
@Indexed
|
||||||
private final Instant instant;
|
private final Instant instant;
|
||||||
Reference in New Issue
Block a user