is.codion.framework.model.DefaultEntitySearchModel Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of codion-framework-model Show documentation
Show all versions of codion-framework-model Show documentation
Codion Application Framework
/*
* This file is part of Codion.
*
* Codion is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Codion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Codion. If not, see .
*
* Copyright (c) 2008 - 2024, Björn Darri Sigurðsson.
*/
package is.codion.framework.model;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.state.State;
import is.codion.common.state.StateObserver;
import is.codion.common.value.Value;
import is.codion.common.value.Value.Notify;
import is.codion.common.value.ValueSet;
import is.codion.framework.db.EntityConnection.Select;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityDefinition;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.condition.Condition;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import static is.codion.framework.domain.entity.condition.Condition.and;
import static is.codion.framework.domain.entity.condition.Condition.or;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Collections.unmodifiableMap;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;
final class DefaultEntitySearchModel implements EntitySearchModel {
private static final Supplier NULL_CONDITION = () -> null;
private static final Function DEFAULT_TO_STRING = Object::toString;
private static final String DEFAULT_SEPARATOR = ",";
private static final String WILDCARD_MULTIPLE = "%";
private static final String WILDCARD_SINGLE = "_";
private final State searchStringModified = State.state();
private final State selectionEmpty = State.state(true);
private final EntityType entityType;
private final Collection> columns;
private final ValueSet entities = ValueSet.builder()
.notify(Notify.WHEN_SET)
.validator(new EntityValidator())
.listener(this::reset)
.consumer(selectedEntities -> selectionEmpty.set(selectedEntities.isEmpty()))
.build();
private final EntityConnectionProvider connectionProvider;
private final Map, Settings> settings;
private final Value searchString = Value.builder()
.nonNull("")
.notify(Notify.WHEN_SET)
.listener(() -> searchStringModified.set(!searchStringRepresentsSelection()))
.build();
private final Value separator = Value.builder()
.nonNull(DEFAULT_SEPARATOR)
.listener(this::reset)
.build();
private final boolean singleSelection;
private final Value> condition = Value.builder()
.nonNull(NULL_CONDITION)
.build();
private final Value> stringFunction = Value.builder()
.nonNull(DEFAULT_TO_STRING)
.build();
private final Value limit;
private final String description;
private DefaultEntitySearchModel(DefaultBuilder builder) {
this.entityType = builder.entityType;
this.connectionProvider = builder.connectionProvider;
this.separator.set(builder.separator);
this.columns = unmodifiableCollection(builder.columns);
this.settings = unmodifiableMap(columns.stream()
.collect(toMap(Function.identity(), column -> new DefaultSettings())));
this.stringFunction.set(builder.stringFunction);
this.description = builder.description == null ? createDescription() : builder.description;
this.singleSelection = builder.singleSelection;
this.limit = Value.value(builder.limit);
}
@Override
public EntityType entityType() {
return entityType;
}
@Override
public EntityConnectionProvider connectionProvider() {
return connectionProvider;
}
@Override
public Collection> columns() {
return columns;
}
@Override
public String description() {
return description;
}
@Override
public Value entity() {
return entities.value();
}
@Override
public ValueSet entities() {
return entities;
}
@Override
public Map, Settings> settings() {
return settings;
}
@Override
public Value limit() {
return limit;
}
@Override
public Value> condition() {
return condition;
}
@Override
public Value> stringFunction() {
return stringFunction;
}
@Override
public void reset() {
searchString.set(entitiesToString());
}
@Override
public List search() {
try {
List result = connectionProvider.connection().select(select());
result.sort(connectionProvider.entities().definition(entityType).comparator());
return result;
}
catch (DatabaseException e) {
throw new RuntimeException(e);
}
}
@Override
public Value searchString() {
return searchString;
}
@Override
public Value separator() {
return separator;
}
@Override
public boolean singleSelection() {
return singleSelection;
}
@Override
public StateObserver searchStringModified() {
return searchStringModified.observer();
}
@Override
public StateObserver selectionEmpty() {
return selectionEmpty.observer();
}
/**
* @return a select instance based on this search model including any additional search condition
* @throws IllegalStateException in case no search columns are specified
*/
private Select select() {
if (columns.isEmpty()) {
throw new IllegalStateException("No search columns provided for search model: " + entityType);
}
Collection conditions = new ArrayList<>();
String[] searchStrings = singleSelection ? new String[] {searchString.get()} : searchString.get().split(separator.get());
for (Column column : columns) {
Settings columnSettings = settings.get(column);
for (String rawSearchString : searchStrings) {
String preparedSearchString = prepareSearchString(rawSearchString, columnSettings);
boolean containsWildcards = containsWildcards(preparedSearchString);
if (columnSettings.caseSensitive().get()) {
conditions.add(containsWildcards ? column.like(preparedSearchString) : column.equalTo(preparedSearchString));
}
else {
conditions.add(containsWildcards ? column.likeIgnoreCase(preparedSearchString) : column.equalToIgnoreCase(preparedSearchString));
}
}
}
return Select.where(createCombinedCondition(conditions))
.limit(limit.get())
.build();
}
private Condition createCombinedCondition(Collection conditions) {
Condition conditionCombination = or(conditions);
Condition additionalCondition = condition.get().get();
return additionalCondition == null ? conditionCombination : and(additionalCondition, conditionCombination);
}
private static String prepareSearchString(String rawSearchString, Settings settings) {
boolean wildcardPrefix = settings.wildcardPrefix().get();
boolean wildcardPostfix = settings.wildcardPostfix().get();
rawSearchString = settings.spaceAsWildcard().get() ? rawSearchString.replace(' ', '%') : rawSearchString;
return rawSearchString.equals(WILDCARD_MULTIPLE) ? WILDCARD_MULTIPLE :
((wildcardPrefix ? WILDCARD_MULTIPLE : "") + rawSearchString.trim() + (wildcardPostfix ? WILDCARD_MULTIPLE : ""));
}
private boolean searchStringRepresentsSelection() {
return (entities.get().isEmpty() && searchString.get().isEmpty()) ||
(!entities.get().isEmpty() && entitiesToString().equals(searchString.get()));
}
private String createDescription() {
EntityDefinition definition = connectionProvider.entities().definition(entityType);
return columns.stream()
.map(column -> definition.columns().definition(column).caption())
.collect(joining(", "));
}
private String entitiesToString() {
return entities.get().stream()
.map(stringFunction.get())
.collect(joining(separator.get()));
}
private void validateType(Entity entity) {
if (!entity.entityType().equals(entityType)) {
throw new IllegalArgumentException("Entities of type " + entityType + " exptected, got " + entity.entityType());
}
}
private final class EntityValidator implements Value.Validator> {
@Override
public void validate(Set entitySet) {
if (entitySet != null) {
if (entitySet.size() > 1 && singleSelection) {
throw new IllegalArgumentException("This EntitySearchModel does not allow the selection of multiple entities");
}
entitySet.forEach(DefaultEntitySearchModel.this::validateType);
}
}
}
private static boolean containsWildcards(String value) {
return value.contains(WILDCARD_MULTIPLE) || value.contains(WILDCARD_SINGLE);
}
private static final class DefaultSettings implements Settings {
private final State wildcardPrefixState = State.state(true);
private final State wildcardPostfixState = State.state(true);
private final State caseSensitiveState = State.state(false);
private final State spaceAsWildcard = State.state(true);
@Override
public State wildcardPrefix() {
return wildcardPrefixState;
}
@Override
public State wildcardPostfix() {
return wildcardPostfixState;
}
@Override
public State spaceAsWildcard() {
return spaceAsWildcard;
}
@Override
public State caseSensitive() {
return caseSensitiveState;
}
}
static final class DefaultBuilder implements Builder {
private final EntityType entityType;
private final EntityConnectionProvider connectionProvider;
private Collection> columns;
private Function stringFunction = DEFAULT_TO_STRING;
private String description;
private boolean singleSelection = false;
private String separator = DEFAULT_SEPARATOR;
private Integer limit = DEFAULT_LIMIT.get();
DefaultBuilder(EntityType entityType, EntityConnectionProvider connectionProvider) {
this.entityType = requireNonNull(entityType);
this.connectionProvider = requireNonNull(connectionProvider);
this.columns = connectionProvider.entities().definition(entityType).columns().searchable();
}
@Override
public Builder columns(Collection> columns) {
if (requireNonNull(columns).isEmpty()) {
throw new IllegalArgumentException("One or more search column is required");
}
validateColumns(columns);
this.columns = columns;
return this;
}
@Override
public Builder stringFunction(Function stringFunction) {
this.stringFunction = requireNonNull(stringFunction);
return this;
}
@Override
public Builder description(String description) {
this.description = requireNonNull(description);
return this;
}
@Override
public Builder singleSelection(boolean singleSelection) {
this.singleSelection = singleSelection;
return this;
}
@Override
public Builder separator(String separator) {
this.separator = requireNonNull(separator);
return this;
}
@Override
public Builder limit(int limit) {
this.limit = limit;
return this;
}
@Override
public EntitySearchModel build() {
return new DefaultEntitySearchModel(this);
}
private void validateColumns(Collection> columns) {
for (Column column : columns) {
if (!entityType.equals(column.entityType())) {
throw new IllegalArgumentException("Column '" + column + "' is not part of entity " + entityType);
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy