Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class ElasticsearchIndexSettingsProvider {

protected final ElasticsearchIndexSettingsConfigurationContext context;

protected final Map<Class<?>, IndexSettings> effectiveIndexSettings;
protected final Map<String, IndexSettings> effectiveIndexSettings;

protected final IndexSettings commonIndexSettings;
protected final IndexSettingsAnalysis commonAnalysisSettings;
Expand All @@ -73,8 +73,11 @@ public ElasticsearchIndexSettingsProvider(List<ElasticsearchIndexSettingsConfigu
}

public IndexSettings getSettingsForIndex(IndexConfiguration indexConfiguration) {
// Cached by entity name: a dynamic entity gets a new Java class on every metadata generation,
// while its settings only depend on the configurers, which do not change at runtime.
String cacheKey = indexConfiguration.getEntityName();
Class<?> entityClass = indexConfiguration.getEntityClass();
IndexSettings resultIndexSettings = this.effectiveIndexSettings.get(entityClass);
IndexSettings resultIndexSettings = this.effectiveIndexSettings.get(cacheKey);
if (resultIndexSettings == null) {
Map<Class<?>, IndexSettings.Builder> indexSettingsBuilders = context.getAllSpecificIndexSettingsBuilders();
IndexSettings entityIndexSettings;
Expand Down Expand Up @@ -153,7 +156,7 @@ public IndexSettings getSettingsForIndex(IndexConfiguration indexConfiguration)
}

resultIndexSettings = deserializeIndexSettings(resultIndexSettingsNode.toString());
this.effectiveIndexSettings.put(entityClass, resultIndexSettings);
this.effectiveIndexSettings.put(cacheKey, resultIndexSettings);
}
return resultIndexSettings;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import io.jmix.core.Id;
import io.jmix.core.IdSerialization;
import io.jmix.core.MetadataTools;
import io.jmix.core.metamodel.model.MetaClass;
import io.jmix.core.querycondition.Condition;
import io.jmix.core.querycondition.JpqlCondition;
import io.jmix.core.querycondition.PropertyCondition;
import io.jmix.flowui.component.filter.SingleFilterComponentBase;
import io.jmix.flowui.model.CollectionLoader;
import io.jmix.flowui.model.DataLoader;
Expand All @@ -46,6 +49,7 @@ public class FullTextFilter extends SingleFilterComponentBase<String> {
protected String parameterName;
protected String searchStrategy;
protected SearchProperties searchProperties;
protected MetadataTools metadataTools;
protected String correctWhere;

@Override
Expand All @@ -54,6 +58,7 @@ protected void autowireDependencies() {
idSerialization = applicationContext.getBean(IdSerialization.class);
entitySearcher = applicationContext.getBean(EntitySearcher.class);
searchProperties = applicationContext.getBean(SearchProperties.class);
metadataTools = applicationContext.getBean(MetadataTools.class);
}

@Override
Expand All @@ -72,10 +77,12 @@ public String getParameterName() {
public void setParameterName(String parameterName) {
checkState(this.parameterName == null, "Parameter name has already been initialized");
checkNotNullArgument(parameterName);
String where = getQueryCondition().getWhere();
if (StringUtils.isNotEmpty(where)) {
correctWhere = where.replace("?", ":" + parameterName);
getQueryCondition().setWhere(correctWhere);
if (queryCondition instanceof JpqlCondition jpqlCondition) {
String where = jpqlCondition.getWhere();
if (StringUtils.isNotEmpty(where)) {
correctWhere = where.replace("?", ":" + parameterName);
jpqlCondition.setWhere(correctWhere);
}
}
this.parameterName = parameterName;
}
Expand All @@ -89,14 +96,26 @@ protected Condition createQueryCondition() {
return fullTextCondition;
}

@Override
public JpqlCondition getQueryCondition() {
return (JpqlCondition) queryCondition;
/**
* Creates the condition for an entity whose store cannot run the JPQL of {@link #createQueryCondition()}. Such a
* store supports an {@code IN} condition on the primary key.
*
* @param metaClass meta class of the entity the data loader selects
* @return condition restricting the primary key to the ids returned by the full text search
*/
protected Condition createNonJpaQueryCondition(MetaClass metaClass) {
PropertyCondition condition = PropertyCondition.inList(
metadataTools.getPrimaryKeyName(metaClass), Collections.emptyList());
// Skippable while empty, so an unfilled filter does not restrict the loader.
condition.setSkipNullOrEmpty(true);
return condition;
}

@Override
protected void updateQueryCondition(@Nullable String newValue) {
if (StringUtils.isEmpty(newValue)) {
// An earlier search that found nothing left the condition always false.
enableCorrectWhereClause();
setQueryConditionParameterValue(Collections.emptyList());
}
}
Expand All @@ -106,6 +125,11 @@ public void setDataLoader(DataLoader dataLoader) {
if (!(dataLoader instanceof CollectionLoader)) {
throw new RuntimeException(FullTextFilter.NAME + " component can only work with CollectionLoader");
}
// The condition is handed to the loader below, so its shape must suit the entity's store.
MetaClass metaClass = ((CollectionLoader<?>) dataLoader).getContainer().getEntityMetaClass();
if (!metadataTools.isJpaEntity(metaClass)) {
queryCondition = createNonJpaQueryCondition(metaClass);
}
super.setDataLoader(dataLoader);
registerDataLoaderPreLoadListener((CollectionLoader<?>) dataLoader);
}
Expand Down Expand Up @@ -149,22 +173,38 @@ private List<Id> performFullTextSearch(String searchTerm) {

/**
* When no data is returned by full-text search we must make the condition return false. We set invalid where
* clause for that purpose.
* clause for that purpose. A property condition instead stops being skippable with an empty list of ids.
*/
private void enableAlwaysFalseWhereClause() {
getQueryCondition().setWhere("1 <> 1");
if (queryCondition instanceof JpqlCondition jpqlCondition) {
jpqlCondition.setWhere("1 <> 1");
} else {
((PropertyCondition) queryCondition).setSkipNullOrEmpty(false);
}
}

private void enableCorrectWhereClause() {
getQueryCondition().setWhere(correctWhere);
if (queryCondition instanceof JpqlCondition jpqlCondition) {
jpqlCondition.setWhere(correctWhere);
} else {
((PropertyCondition) queryCondition).setSkipNullOrEmpty(true);
}
}

private void setQueryConditionParameterValue(List<Object> value) {
getQueryCondition().setParameterValuesMap(Collections.singletonMap(parameterName, value));
if (queryCondition instanceof JpqlCondition jpqlCondition) {
jpqlCondition.setParameterValuesMap(Collections.singletonMap(parameterName, value));
} else {
((PropertyCondition) queryCondition).setParameterValue(value);
}
}

private void clearConditionParameterValuesMap() {
getQueryCondition().setParameterValuesMap(Collections.emptyMap());
if (queryCondition instanceof JpqlCondition jpqlCondition) {
jpqlCondition.setParameterValuesMap(Collections.emptyMap());
} else {
((PropertyCondition) queryCondition).setParameterValue(Collections.emptyList());
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import io.jmix.flowui.Notifications;
import io.jmix.flowui.UiComponents;
import io.jmix.flowui.ViewNavigators;
import io.jmix.flowui.exception.NoSuchViewException;
import io.jmix.flowui.kit.component.button.JmixButton;
import io.jmix.flowui.theme.StyleUtility;
import io.jmix.flowui.view.*;
Expand Down Expand Up @@ -92,6 +93,8 @@ public class SearchResultsView extends StandardView {
protected SearchProperties searchProperties;
@Autowired
protected MetadataTools metadataTools;
@Autowired
protected ViewRegistry viewRegistry;

protected SearchResult searchResult;
protected String searchStrategy;
Expand Down Expand Up @@ -288,6 +291,14 @@ protected JmixButton createInstanceButton(String entityName, SearchResultEntry e

protected void openEntityView(SearchResultEntry entry, String entityName) {
MetaClass metaClass = metadata.getSession().getClass(entityName);
if (!hasDetailView(metaClass)) {
// A runtime-defined entity may have no detail view at all.
String message = messageBundle.formatMessage("noDetailView", messageTools.getEntityCaption(metaClass));
notifications.create(message)
.withType(Notifications.Type.WARNING)
.show();
return;
}
Object entity = reloadEntity(metaClass, idSerialization.stringToId(entry.getDocId()));
if (OpenMode.DIALOG.equals(searchFieldContext.getOpenMode())) {
dialogWindows.detail(this, metaClass.getJavaClass())
Expand All @@ -301,6 +312,15 @@ protected void openEntityView(SearchResultEntry entry, String entityName) {
}
}

protected boolean hasDetailView(MetaClass metaClass) {
try {
viewRegistry.getDetailViewInfo(metaClass);
return true;
} catch (NoSuchViewException e) {
return false;
}
}

protected final ComponentRenderer<Component, SearchResultEntry> searchResultRenderer = new ComponentRenderer<>(entry -> {
VerticalLayout verticalLayout = uiComponents.create(VerticalLayout.class);
verticalLayout.setWidthFull();
Expand Down Expand Up @@ -367,7 +387,8 @@ protected String formatFieldCaption(String entityName, String fieldName) {
protected Object reloadEntity(MetaClass metaClass, Object entityId) {
return dataManager
.load(metaClass.getJavaClass())
.id(entityId)
// A store is given the identifier value; only the JPA store unwraps an Id itself.
.id(entityId instanceof Id<?> id ? id.getValue() : entityId)
.fetchPlan(FetchPlan.LOCAL)
.one();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ io.jmix.searchflowui.view.result/fileName=File name
io.jmix.searchflowui.view.result/content=File content
io.jmix.searchflowui.view.result/noResults=No results
io.jmix.searchflowui.view.result/searchDisabled=Search add-on is disabled
io.jmix.searchflowui.view.result/noDetailView=There is no detail view for %s

io.jmix.searchflowui.view.filter/fullTextFilterConditionDetailView.title=Full text filter condition detail view
io.jmix.searchflowui.view.filter/defaultLabel=Full-text criteria
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2026 Haulmont.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.jmix.searchflowui.component;

import io.jmix.core.MetadataTools;
import io.jmix.core.metamodel.model.MetaClass;
import io.jmix.core.querycondition.Condition;
import io.jmix.core.querycondition.JpqlCondition;
import io.jmix.core.querycondition.LogicalCondition;
import io.jmix.core.querycondition.PropertyCondition;
import io.jmix.flowui.model.CollectionContainer;
import io.jmix.flowui.model.CollectionLoader;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.Set;

/**
* The component filters by the ids the search returned. A store that cannot run JPQL rejects the JPQL condition, so
* such an entity is filtered with a property condition on its primary key instead.
*/
public class FullTextFilterConditionTest {

@Test
void jpaEntityKeepsTheJpqlCondition() {
TestFullTextFilter filter = new TestFullTextFilter(metadataTools(true));

filter.setDataLoader(dataLoader());

Assertions.assertInstanceOf(JpqlCondition.class, filter.getQueryCondition());
Assertions.assertEquals("{E}.id in :testParameter",
((JpqlCondition) filter.getQueryCondition()).getWhere());
}

@Test
void nonJpaEntityIsFilteredByAnInListConditionOnThePrimaryKey() {
TestFullTextFilter filter = new TestFullTextFilter(metadataTools(false));

filter.setDataLoader(dataLoader());

Condition condition = filter.getQueryCondition();
Assertions.assertInstanceOf(PropertyCondition.class, condition);
PropertyCondition propertyCondition = (PropertyCondition) condition;
Assertions.assertEquals("id", propertyCondition.getProperty());
Assertions.assertEquals(PropertyCondition.Operation.IN_LIST, propertyCondition.getOperation());
}

@Test
void nonJpaConditionIsSkippedWhileTheFilterIsEmpty() {
TestFullTextFilter filter = new TestFullTextFilter(metadataTools(false));
filter.setDataLoader(dataLoader());

// A condition that is skipped leaves the loader unrestricted, which is what an unfilled filter must do.
Assertions.assertNull(filter.getQueryCondition().actualize(Set.of(), false));
}

@Test
void clearingTheFilterRestoresTheSkippedNonJpaCondition() {
TestFullTextFilter filter = new TestFullTextFilter(metadataTools(false));
filter.setDataLoader(dataLoader());
PropertyCondition condition = (PropertyCondition) filter.getQueryCondition();

// A search that found nothing leaves the condition always false: applied, with no ids to match.
condition.setSkipNullOrEmpty(false);
Assertions.assertSame(condition, condition.actualize(Set.of(), false));

filter.updateQueryCondition(null);

Assertions.assertNull(condition.actualize(Set.of(), false),
"clearing the filter must stop the previous empty result from hiding every row");
}

private MetadataTools metadataTools(boolean jpaEntity) {
MetadataTools metadataTools = Mockito.mock(MetadataTools.class);
Mockito.when(metadataTools.isJpaEntity(Mockito.any(MetaClass.class))).thenReturn(jpaEntity);
Mockito.when(metadataTools.getPrimaryKeyName(Mockito.any(MetaClass.class))).thenReturn("id");
return metadataTools;
}

private CollectionLoader<?> dataLoader() {
CollectionContainer<?> container = Mockito.mock(CollectionContainer.class);
Mockito.when(container.getEntityMetaClass()).thenReturn(Mockito.mock(MetaClass.class));

CollectionLoader<?> dataLoader = Mockito.mock(CollectionLoader.class);
Mockito.when(dataLoader.getContainer()).thenAnswer(invocation -> container);
Mockito.when(dataLoader.getCondition()).thenReturn(LogicalCondition.and());
return dataLoader;
}

/**
* Builds the component without a Spring context, which {@code initComponent()} would need.
*/
private static class TestFullTextFilter extends FullTextFilter {

TestFullTextFilter(MetadataTools metadataTools) {
this.metadataTools = metadataTools;
this.queryCondition = createQueryCondition();
setParameterName("testParameter");
}

@Override
public void updateQueryCondition(String newValue) {
super.updateQueryCondition(newValue);
}
}
}
Loading