All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.primefaces.extensions.model.mongo.MorphiaLazyDataModel Maven / Gradle / Ivy

There is a newer version: 14.0.7.1
Show newest version
/*
 * Copyright (c) 2011-2023 PrimeFaces Extensions
 *
 *  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 org.primefaces.extensions.model.mongo;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
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.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;

/**
 * Basic {@link LazyDataModel} implementation for MongoDB using Morphia.
 *
 * @param  The model class.
 */
public class MorphiaLazyDataModel extends LazyDataModel implements Serializable {

    protected Class entityClass;
    protected SerializableSupplier ds;
    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;

    /*
     * if the default match mode queries in applyFilters() dont work for a specific field, overridden field queries with the overrideFieldQuery method will add
     * BiConsumers to this map, where the key is the field name specified in 
     */
    private final Map, FilterMeta>> overrides = new HashMap<>();
    // consumer to be executed before the query is built, useful to modify the original query
    private transient Consumer> prependConsumer;
    // global filter consumer (to be implemented by the user)
    private transient BiConsumer, FilterMeta> globalFilterConsumer;

    /**
     * For serialization only
     */
    public MorphiaLazyDataModel() {
        // for serialization only
    }

    /**
     * Constructs a Morphia lazy data model with selection support.
     *
     * @param ds the {@link Datastore}
     * @param entityClass The entity class
     * @param rowKeyField The name of the rowKey property (e.g. "id")
     */
    public MorphiaLazyDataModel(final Class entityClass, final SerializableSupplier ds,
                final String rowKeyField) {
        this();
        this.ds = ds;
        this.entityClass = entityClass;
        this.rowKeyField = rowKeyField;
    }

    /**
     * Constructs a Morphia lazy data model with selection support with the default "id" field being the row key.
     *
     * @param ds the {@link Datastore}
     * @param entityClass The entity class
     */
    public MorphiaLazyDataModel(final Class entityClass, final SerializableSupplier ds) {
        this(entityClass, ds, "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);
                }
            }
            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]."));
    }

    @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]."));
    }

    @Override
    public int count(final Map map) {
        final Query q = this.buildQuery();
        final long count = applyFilters(q, map).count();
        return (int) count;
    }

    @Override
    public List load(final int first, final int pageSize, final Map sort,
                final Map filters) {
        final Query q = this.buildQuery();
        final FindOptions opt = new FindOptions();
        sort.forEach((field, sortData) -> opt.sort(sortData.getOrder().name().equalsIgnoreCase("DESCENDING") ? Sort.descending(field) : Sort.ascending(field)));

        this.applyFilters(q, filters);
        opt.skip(first).limit(pageSize);
        return q.iterator(opt).toList();
    }

    public Query applyFilters(final Query q, final Map filters) {
        filters.forEach((field, metadata) -> {

            if (metadata.getFilterValue() != null) {
                final BiConsumer, FilterMeta> override = overrides.get(field);
                if (override != null) {
                    override.accept(q, metadata);
                }
                else {
                    final Object val = metadata.getFilterValue();
                    if (metadata.getMatchMode() != null) {

                        switch (metadata.getMatchMode()) {
                            case STARTS_WITH:
                                final RegexFilter regStartsWith = Filters.regex(field);
                                regStartsWith.pattern("^" + val).caseInsensitive();
                                q.filter(regStartsWith);
                                break;
                            case ENDS_WITH:
                                final RegexFilter regEndsWith = Filters.regex(field);
                                regEndsWith.pattern(val + "$").caseInsensitive();
                                q.filter(regEndsWith);
                                break;
                            case CONTAINS:
                                q.filter(Filters.regex(field).pattern(val + "").caseInsensitive());
                                break;
                            case EXACT:
                                final Object castedValueEx = castedValue(field, val);
                                if (castedValueEx != null) {
                                    q.filter(Filters.eq(field, castedValueEx));
                                }
                                else {
                                    q.filter(Filters.eq(field, val));
                                }
                                break;
                            case LESS_THAN:
                                final Object castedValueLt = castedValue(field, val);
                                if (castedValueLt != null) {
                                    q.filter(Filters.lt(field, castedValueLt));
                                }
                                else {
                                    q.filter(Filters.lt(field, val));
                                }
                                break;
                            case LESS_THAN_EQUALS:
                                final Object castedValueLte = castedValue(field, val);
                                if (castedValueLte != null) {
                                    q.filter(Filters.lte(field, castedValueLte));
                                }
                                else {
                                    q.filter(Filters.lte(field, val));
                                }
                                break;
                            case GREATER_THAN:
                                final Object castedValueGt = castedValue(field, val);
                                if (castedValueGt != null) {
                                    q.filter(Filters.gt(field, castedValueGt));
                                }
                                else {
                                    q.filter(Filters.gt(field, val));
                                }
                                break;
                            case GREATER_THAN_EQUALS:

                                final Object castedValueGte = castedValue(field, val);
                                if (castedValueGte != null) {
                                    q.filter(Filters.gte(field, castedValueGte));
                                }
                                else {
                                    q.filter(Filters.gte(field, val));
                                }
                                break;
                            case EQUALS:
                                q.filter(Filters.eq(field, val));
                                break;
                            case IN:
                                if (metadata.getFilterValue().getClass() == Object[].class) {
                                    final Object[] parts = (Object[]) metadata.getFilterValue();
                                    q.filter(Filters.in(field, Arrays.asList(parts)));
                                }
                                break;
                            case BETWEEN:
                                if (metadata.getFilterValue() instanceof List) {
                                    final List dates = (List) metadata.getFilterValue();
                                    if (dates.size() > 1) { // does this ever have less than 2 items?
                                        q.filter(Filters.gte(field, dates.get(0)), Filters.lte(field, dates.get(1)));
                                    }
                                }
                                break;
                            case NOT_CONTAINS:
                                q.filter(Filters.regex(field).pattern(val + Constants.EMPTY_STRING).caseInsensitive()
                                            .not());
                                break;
                            case NOT_EQUALS:
                                final Object castedValueNe = castedValue(field, val);
                                if (castedValueNe != null) {
                                    q.filter(Filters.eq(field, castedValueNe).not());
                                }
                                else {
                                    q.filter(Filters.eq(field, val).not());
                                }
                                break;
                            case NOT_STARTS_WITH:
                                final RegexFilter regStartsWithNot = Filters.regex(field);
                                regStartsWithNot.pattern("^" + val).caseInsensitive();
                                q.filter(regStartsWithNot.not());
                                break;
                            case NOT_IN:
                                if (metadata.getFilterValue() instanceof Object[]) {
                                    final Object[] parts = (Object[]) metadata.getFilterValue();
                                    q.filter(Filters.nin(field, Arrays.asList(parts)));
                                }
                                break;
                            case NOT_ENDS_WITH:
                                final RegexFilter regEndsWithNot = Filters.regex(field);
                                regEndsWithNot.pattern(val + "$").caseInsensitive();
                                q.filter(regEndsWithNot.not());
                                break;
                            case GLOBAL:
                                if (globalFilterConsumer != null) {
                                    globalFilterConsumer.accept(q, metadata);
                                }
                                break;

                            default:
                                throw new UnsupportedOperationException(
                                            "MatchMode " + metadata.getMatchMode() + " not supported");
                        }
                    }
                }
            }
        });
        return q;
    }

    public MorphiaLazyDataModel prependQuery(final Consumer> consumer) {
        this.prependConsumer = consumer;
        return this;
    }

    public MorphiaLazyDataModel globalFilter(final BiConsumer, FilterMeta> consumer) {
        this.globalFilterConsumer = consumer;
        return this;
    }

    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
     * fields which are numbers like ints,floats,doubles and longs where the filterValue will be a string and thus the query wont find any matches since it's
     * comparing a string with a number
     *
     * @param field the field on the entity to cast
     * @param value the value to cast based on the field
     * @return the newly cast object
     */
    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 + "";
            }
        }
        catch (final Exception 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();
        if (prependConsumer != null) {
            prependConsumer.accept(q);
        }
        return q;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy