diff --git a/core/src/main/java/org/primefaces/extensions/model/mongo/MorphiaLazyDataModel.java b/core/src/main/java/org/primefaces/extensions/model/mongo/MorphiaLazyDataModel.java index 78e053dc5..0edbf021a 100644 --- a/core/src/main/java/org/primefaces/extensions/model/mongo/MorphiaLazyDataModel.java +++ b/core/src/main/java/org/primefaces/extensions/model/mongo/MorphiaLazyDataModel.java @@ -21,35 +21,38 @@ */ package org.primefaces.extensions.model.mongo; -import java.beans.IntrospectionException; -import java.beans.PropertyDescriptor; +import dev.morphia.Datastore; +import dev.morphia.query.FindOptions; +import dev.morphia.query.MorphiaCursor; +import dev.morphia.query.Query; +import dev.morphia.query.Sort; +import dev.morphia.query.filters.Filters; +import dev.morphia.query.filters.RegexFilter; +import org.primefaces.context.PrimeApplicationContext; +import org.primefaces.model.FilterMeta; +import org.primefaces.model.LazyDataModel; +import org.primefaces.model.SortMeta; +import org.primefaces.model.SortOrder; +import org.primefaces.util.ComponentUtils; +import org.primefaces.util.Constants; +import org.primefaces.util.PropertyDescriptorResolver; +import org.primefaces.util.SerializableFunction; +import org.primefaces.util.SerializableSupplier; + +import javax.faces.FacesException; +import javax.faces.context.FacesContext; +import javax.faces.convert.Converter; import java.io.Serializable; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Consumer; - -import javax.faces.FacesException; - -import org.primefaces.model.FilterMeta; -import org.primefaces.model.LazyDataModel; -import org.primefaces.model.SortMeta; -import org.primefaces.util.Constants; -import org.primefaces.util.Lazy; -import org.primefaces.util.SerializableSupplier; - -import dev.morphia.Datastore; -import dev.morphia.query.FindOptions; -import dev.morphia.query.Query; -import dev.morphia.query.Sort; -import dev.morphia.query.filters.Filters; -import dev.morphia.query.filters.RegexFilter; +import java.util.logging.Logger; /** * Basic {@link LazyDataModel} implementation for MongoDB using Morphia. @@ -58,12 +61,12 @@ */ public class MorphiaLazyDataModel extends LazyDataModel implements Serializable { + private static final Logger LOGGER = Logger.getLogger(MorphiaLazyDataModel.class.getName()); + protected Class entityClass; - protected SerializableSupplier ds; + protected SerializableSupplier datastore; protected String rowKeyField; - - // usually will be getId() but can be a user specified method as well when using the 2nd constructor - private transient Lazy rowKeyGetter; + protected SerializableFunction rowKeyProvider; /* * if the default match mode queries in applyFilters() dont work for a specific field, overridden field queries with the overrideFieldQuery method will add @@ -85,14 +88,17 @@ public MorphiaLazyDataModel() { /** * Constructs a Morphia lazy data model with selection support. * - * @param ds the {@link Datastore} + * @param datastore the {@link Datastore} * @param entityClass The entity class * @param rowKeyField The name of the rowKey property (e.g. "id") + * + * @deprecated use {@link MorphiaLazyDataModel#builder()} instead */ - public MorphiaLazyDataModel(final Class entityClass, final SerializableSupplier ds, + @Deprecated + public MorphiaLazyDataModel(final Class entityClass, final SerializableSupplier datastore, final String rowKeyField) { this(); - this.ds = ds; + this.datastore = datastore; this.entityClass = entityClass; this.rowKeyField = rowKeyField; } @@ -100,52 +106,32 @@ public MorphiaLazyDataModel(final Class entityClass, final SerializableSuppli /** * Constructs a Morphia lazy data model with selection support with the default "id" field being the row key. * - * @param ds the {@link Datastore} + * @param datastore the {@link Datastore} * @param entityClass The entity class + * + * @deprecated use {@link MorphiaLazyDataModel#builder()} instead */ - public MorphiaLazyDataModel(final Class entityClass, final SerializableSupplier ds) { - this(entityClass, ds, "id"); + @Deprecated + public MorphiaLazyDataModel(final Class entityClass, final SerializableSupplier datastore) { + this(entityClass, datastore, "id"); } @Override public T getRowData(final String rowKey) { - if (rowKeyField != null) { - for (final T object : getWrappedData()) { - try { - final Object rko = getRowKeyGetter().invoke(object); - final String rk = rko == null ? null : rko.toString(); - if (Objects.equals(rk, rowKey)) { - return object; - } - } - catch (final Exception ex) { - throw new FacesException( - "Could not invoke getter for " + rowKeyField + " on " + entityClass.getName(), ex); - } + List values = Objects.requireNonNullElseGet(getWrappedData(), Collections::emptyList); + for (T obj : values) { + if (Objects.equals(rowKey, getRowKey(obj))) { + return obj; } - return null; } - throw new UnsupportedOperationException( - getMessage("Provide a Converter or rowKeyField via constructor or implement getRowData(String rowKey) in %s" - + ", when basic rowKey algorithm is not used [component=%s,view=%s].")); + + return null; } @Override public String getRowKey(final T object) { - if (rowKeyField != null) { - try { - final Object rowKey = getRowKeyGetter().invoke(object); - return rowKey == null ? null : rowKey.toString(); - } - catch (final InvocationTargetException | IllegalAccessException e) { - throw new FacesException("Could not invoke getter for " + rowKeyField + " on " + entityClass.getName(), - e); - } - } - - throw new UnsupportedOperationException( - getMessage("Provide a Converter or rowKeyField via constructor or implement getRowKey(T object) in %s" - + ", when basic rowKey algorithm is not used [component=%s,view=%s].")); + Object rowKey = rowKeyProvider.apply(object); + return rowKey == null ? null : String.valueOf(rowKey); } @Override @@ -158,13 +144,15 @@ public int count(final Map map) { @Override public List load(final int first, final int pageSize, final Map sort, final Map filters) { - final Query q = this.buildQuery(); + final Query q = buildQuery(); final FindOptions opt = new FindOptions(); - sort.forEach((field, sortData) -> opt.sort(sortData.getOrder().name().equalsIgnoreCase("DESCENDING") ? Sort.descending(field) : Sort.ascending(field))); + sort.forEach((field, sortData) -> opt.sort(sortData.getOrder() == SortOrder.DESCENDING ? Sort.descending(field) : Sort.ascending(field))); - this.applyFilters(q, filters); + applyFilters(q, filters); opt.skip(first).limit(pageSize); - return q.iterator(opt).toList(); + try (MorphiaCursor cursor = q.iterator(opt)) { + return cursor.toList(); + } } public Query applyFilters(final Query q, final Map filters) { @@ -181,17 +169,15 @@ public Query applyFilters(final Query q, final Map fil switch (metadata.getMatchMode()) { case STARTS_WITH: - final RegexFilter regStartsWith = Filters.regex(field); - regStartsWith.pattern("^" + val).caseInsensitive(); + final RegexFilter regStartsWith = Filters.regex(field, "^" + val).caseInsensitive(); q.filter(regStartsWith); break; case ENDS_WITH: - final RegexFilter regEndsWith = Filters.regex(field); - regEndsWith.pattern(val + "$").caseInsensitive(); + final RegexFilter regEndsWith = Filters.regex(field, val + "$").caseInsensitive(); q.filter(regEndsWith); break; case CONTAINS: - q.filter(Filters.regex(field).pattern(val + "").caseInsensitive()); + q.filter(Filters.regex(field, val + "").caseInsensitive()); break; case EXACT: final Object castedValueEx = castedValue(field, val); @@ -257,8 +243,7 @@ public Query applyFilters(final Query q, final Map fil } break; case NOT_CONTAINS: - q.filter(Filters.regex(field).pattern(val + Constants.EMPTY_STRING).caseInsensitive() - .not()); + q.filter(Filters.regex(field, val + Constants.EMPTY_STRING).caseInsensitive().not()); break; case NOT_EQUALS: final Object castedValueNe = castedValue(field, val); @@ -270,8 +255,7 @@ public Query applyFilters(final Query q, final Map fil } break; case NOT_STARTS_WITH: - final RegexFilter regStartsWithNot = Filters.regex(field); - regStartsWithNot.pattern("^" + val).caseInsensitive(); + final RegexFilter regStartsWithNot = Filters.regex(field, "^" + val).caseInsensitive(); q.filter(regStartsWithNot.not()); break; case NOT_IN: @@ -281,8 +265,7 @@ public Query applyFilters(final Query q, final Map fil } break; case NOT_ENDS_WITH: - final RegexFilter regEndsWithNot = Filters.regex(field); - regEndsWithNot.pattern(val + "$").caseInsensitive(); + final RegexFilter regEndsWithNot = Filters.regex(field, val + "$").caseInsensitive(); q.filter(regEndsWithNot.not()); break; case GLOBAL: @@ -302,36 +285,34 @@ public Query applyFilters(final Query q, final Map fil return q; } + /** + * use {@link Builder#prependQuery(Consumer)} instead + */ + @Deprecated public MorphiaLazyDataModel prependQuery(final Consumer> consumer) { this.prependConsumer = consumer; return this; } + /** + * use {@link Builder#globalFilter(BiConsumer)} instead + */ + @Deprecated public MorphiaLazyDataModel globalFilter(final BiConsumer, FilterMeta> consumer) { this.globalFilterConsumer = consumer; return this; } + /** + * use {@link Builder#overrideFieldQuery(String, BiConsumer)} instead + */ + @Deprecated public MorphiaLazyDataModel overrideFieldQuery(final String field, final BiConsumer, FilterMeta> consumer) { this.overrides.put(field, consumer); return this; } - protected Method getRowKeyGetter() { - if (rowKeyGetter == null) { - rowKeyGetter = new Lazy<>(() -> { - try { - return new PropertyDescriptor(rowKeyField, entityClass).getReadMethod(); - } - catch (final IntrospectionException e) { - throw new FacesException("Could not access " + rowKeyField + " on " + entityClass.getName(), e); - } - }); - } - return rowKeyGetter.get(); - } - /** * checks the data type of the field on the corresponding class and tries to convert the string value to its data type (only handles basic primitive types, * for more complex data types the field query should be overridden with the overrideFieldQuery method) for example this can be useful when filtering for @@ -344,41 +325,93 @@ protected Method getRowKeyGetter() { */ private Object castedValue(final String field, final Object value) { try { - final Field f = entityClass.getDeclaredField(field); - if (f == null) { - return null; - } - if (f.getType().isAssignableFrom(Integer.class) || f.getType().isAssignableFrom(int.class)) { - return Integer.valueOf(value + ""); - } - else if (f.getType().isAssignableFrom(Float.class) || f.getType().isAssignableFrom(float.class)) { - return Float.valueOf(value + ""); - } - else if (f.getType().isAssignableFrom(Double.class) || f.getType().isAssignableFrom(double.class)) { - return Double.valueOf(value + ""); - } - else if (f.getType().isAssignableFrom(Long.class) || f.getType().isAssignableFrom(long.class)) { - return Long.valueOf(value + ""); - } - else if (f.getType().isAssignableFrom(Boolean.class) || f.getType().isAssignableFrom(boolean.class)) { - return Boolean.valueOf(value + ""); - } - else if (f.getType().isAssignableFrom(String.class)) { - return value + ""; - } + final Field declaredField = entityClass.getDeclaredField(field); + return ComponentUtils.convertToType(value, declaredField.getType(), LOGGER); } - catch (final Exception e) { + catch (final ReflectiveOperationException e) { throw new FacesException("Failed to convert " + field + " to its corresponding data type", e); } - return null; } private Query buildQuery() { - final Query q = ds.get().find(entityClass).disableValidation(); + final Query q = datastore.get().find(entityClass).disableValidation(); if (prependConsumer != null) { prependConsumer.accept(q); } return q; } + public static Builder builder() { + return new Builder<>(); + } + + public static class Builder { + private final MorphiaLazyDataModel model; + + public Builder() { + model = new MorphiaLazyDataModel<>(); + } + + public Builder entityClass(Class entityClass) { + model.entityClass = entityClass; + return this; + } + + public Builder entityManager(SerializableSupplier datastore) { + model.datastore = datastore; + return this; + } + + public Builder rowKeyConverter(Converter rowKeyConverter) { + model.rowKeyConverter = rowKeyConverter; + return this; + } + + public Builder rowKeyProvider(SerializableFunction rowKeyProvider) { + model.rowKeyProvider = rowKeyProvider; + return this; + } + + public Builder rowKeyField(String rowKey) { + model.rowKeyField = rowKey; + return this; + } + + public Builder prependQuery(final Consumer> consumer) { + model.prependConsumer = consumer; + return this; + } + + public Builder globalFilter(final BiConsumer, FilterMeta> consumer) { + model.globalFilterConsumer = consumer; + return this; + } + + public Builder overrideFieldQuery(final String field, final BiConsumer, FilterMeta> consumer) { + model.overrides.put(field, consumer); + return this; + } + + public MorphiaLazyDataModel build() { + Objects.requireNonNull(model.entityClass, "entityClass not set"); + Objects.requireNonNull(model.datastore, "datastore not set"); + + boolean selectionEnabled = model.rowKeyProvider != null || model.rowKeyConverter != null || model.rowKeyField != null; + if (selectionEnabled) { + Objects.requireNonNull(model.rowKeyField, "rowKeyField is mandatory for selection"); + + if (model.rowKeyProvider == null) { + if (model.rowKeyConverter != null) { + model.rowKeyProvider = model::getRowKeyFromConverter; + } + else { + PropertyDescriptorResolver propResolver = + PrimeApplicationContext.getCurrentInstance(FacesContext.getCurrentInstance()).getPropertyDescriptorResolver(); + model.rowKeyProvider = obj -> propResolver.getValue(obj, model.rowKeyField); + } + } + } + return model; + } + } } diff --git a/pom.xml b/pom.xml index 1dd1897dd..d08c55c1e 100644 --- a/pom.xml +++ b/pom.xml @@ -95,9 +95,9 @@ ${project.build.directory}/classes/META-INF/resources/primefaces-extensions - 8 - 8 - 8 + 11 + 11 + 11 ISO-8859-1 ${project.basedir} Development