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

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

The newest version!
/*
 * Copyright (c) 2011-2024 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.PropertyDescriptor;
import java.io.Serializable;
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 java.util.logging.Logger;

import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

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.Callbacks;
import org.primefaces.util.ComponentUtils;
import org.primefaces.util.Constants;
import org.primefaces.util.PropertyDescriptorResolver;

import dev.morphia.Datastore;
import dev.morphia.query.CountOptions;
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;

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

    private static final Logger LOGGER = Logger.getLogger(MorphiaLazyDataModel.class.getName());

    protected Class entityClass;
    protected Callbacks.SerializableSupplier datastore;
    protected String rowKeyField;
    protected Callbacks.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
     * 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 user supplied FindOptions
    private transient Callbacks.SerializableSupplier findOptionsSupplier;
    // for user supplied CountOptions
    private transient Callbacks.SerializableSupplier countOptionsSupplier;

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

    @Override
    public T getRowData(final String rowKey) {
        List values = Objects.requireNonNullElseGet(getWrappedData(), Collections::emptyList);
        for (T obj : values) {
            if (Objects.equals(rowKey, getRowKey(obj))) {
                return obj;
            }
        }

        return null;
    }

    @Override
    public String getRowKey(final T object) {
        return String.valueOf(rowKeyProvider.apply(object));
    }

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

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

        applyFilters(q, filters);
        opt.skip(first).limit(pageSize);
        try (MorphiaCursor cursor = q.iterator(opt)) {
            return cursor.toList();
        }
    }

    protected FindOptions getFindOptions() {
        try {
            return findOptionsSupplier != null ? findOptionsSupplier.get() : new FindOptions();
        }
        catch (Exception e) {
            // if we get here, this means the user supplied FindOptions failed to resolve for some reason, so we fall back to the default
            return new FindOptions();
        }
    }

    protected CountOptions getCountOptions() {
        try {
            return countOptionsSupplier != null ? countOptionsSupplier.get() : new CountOptions();
        }
        catch (Exception e) {
            // if we get here, this means the user supplied CountOptions failed to resolve for some reason, so we fall back to the default
            return new CountOptions();
        }
    }

    public Query applyFilters(final Query q, final Map filters) {
        PrimeApplicationContext primeAppContext = PrimeApplicationContext.getCurrentInstance(FacesContext.getCurrentInstance());

        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, "^" + val).caseInsensitive();
                                q.filter(regStartsWith);
                                break;
                            case ENDS_WITH:
                                final RegexFilter regEndsWith = Filters.regex(field, val + "$").caseInsensitive();
                                q.filter(regEndsWith);
                                break;
                            case CONTAINS:
                                q.filter(Filters.regex(field, val + "").caseInsensitive());
                                break;
                            case EXACT:
                                final Object castedValueEx = convertToDataType(primeAppContext, 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 = convertToDataType(primeAppContext, 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 = convertToDataType(primeAppContext, 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 = convertToDataType(primeAppContext, 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 = convertToDataType(primeAppContext, 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, val + Constants.EMPTY_STRING).caseInsensitive().not());
                                break;
                            case NOT_EQUALS:
                                final Object castedValueNe = convertToDataType(primeAppContext, 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, "^" + 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, 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;
    }

    /**
     * use {@link Builder#prependQuery(Consumer)} instead
     */
    @Deprecated
    public MorphiaLazyDataModel prependQuery(final Consumer> consumer) {
        this.prependConsumer = consumer;
        return this;
    }

    /**
     * use {@link Builder#findOptions(Callbacks.SerializableSupplier)} instead
     */
    @Deprecated
    public MorphiaLazyDataModel findOptions(final Callbacks.SerializableSupplier supplier) {
        this.findOptionsSupplier = supplier;
        return this;
    }

    /**
     * use {@link Builder#findOptions(Callbacks.SerializableSupplier)} instead
     */
    @Deprecated
    public MorphiaLazyDataModel countOptions(final Callbacks.SerializableSupplier supplier) {
        this.countOptionsSupplier = supplier;
        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;
    }

    private Object convertToDataType(PrimeApplicationContext primeAppContext, final String field, final Object value) {
        PropertyDescriptorResolver propResolver = primeAppContext.getPropertyDescriptorResolver();
        final PropertyDescriptor propertyDescriptor = propResolver.get(entityClass, field);
        return ComponentUtils.convertToType(value, propertyDescriptor.getPropertyType(), LOGGER);
    }

    private Query buildQuery() {
        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 datastore(Callbacks.SerializableSupplier datastore) {
            model.datastore = datastore;
            return this;
        }

        public Builder rowKeyConverter(Converter rowKeyConverter) {
            model.rowKeyConverter = rowKeyConverter;
            return this;
        }

        public Builder rowKeyProvider(Callbacks.SerializableFunction rowKeyProvider) {
            model.rowKeyProvider = rowKeyProvider;
            return this;
        }

        public Builder rowKeyField(String rowKey) {
            model.rowKeyField = rowKey;
            return this;
        }

        public Builder findOptions(Callbacks.SerializableSupplier findOptionsSupplier) {
            model.findOptionsSupplier = findOptionsSupplier;
            return this;
        }

        public Builder countOptions(Callbacks.SerializableSupplier countOptionsSupplier) {
            model.countOptionsSupplier = countOptionsSupplier;
            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 requiresRowKeyProvider = model.rowKeyProvider == null && (model.rowKeyConverter != null || model.rowKeyField != null);
            if (requiresRowKeyProvider) {
                if (model.rowKeyConverter != null) {
                    model.rowKeyProvider = model::getRowKeyFromConverter;
                }
                else {
                    Objects.requireNonNull(model.rowKeyField, "rowKeyField is mandatory if neither rowKeyProvider nor converter is provided");

                    PropertyDescriptorResolver propResolver = PrimeApplicationContext.getCurrentInstance(FacesContext.getCurrentInstance())
                                .getPropertyDescriptorResolver();
                    model.rowKeyProvider = obj -> propResolver.getValue(obj, model.rowKeyField);
                }
            }
            return model;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy