View Javadoc
1   /*
2    * Copyright (c) 2011-2024 PrimeFaces Extensions
3    *
4    *  Permission is hereby granted, free of charge, to any person obtaining a copy
5    *  of this software and associated documentation files (the "Software"), to deal
6    *  in the Software without restriction, including without limitation the rights
7    *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    *  copies of the Software, and to permit persons to whom the Software is
9    *  furnished to do so, subject to the following conditions:
10   *
11   *  The above copyright notice and this permission notice shall be included in
12   *  all copies or substantial portions of the Software.
13   *
14   *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20   *  THE SOFTWARE.
21   */
22  package org.primefaces.extensions.model.mongo;
23  
24  import java.beans.PropertyDescriptor;
25  import java.io.Serializable;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Objects;
32  import java.util.function.BiConsumer;
33  import java.util.function.Consumer;
34  import java.util.logging.Logger;
35  
36  import javax.faces.context.FacesContext;
37  import javax.faces.convert.Converter;
38  
39  import org.primefaces.context.PrimeApplicationContext;
40  import org.primefaces.model.FilterMeta;
41  import org.primefaces.model.LazyDataModel;
42  import org.primefaces.model.SortMeta;
43  import org.primefaces.model.SortOrder;
44  import org.primefaces.util.Callbacks;
45  import org.primefaces.util.ComponentUtils;
46  import org.primefaces.util.Constants;
47  import org.primefaces.util.PropertyDescriptorResolver;
48  
49  import dev.morphia.Datastore;
50  import dev.morphia.query.CountOptions;
51  import dev.morphia.query.FindOptions;
52  import dev.morphia.query.MorphiaCursor;
53  import dev.morphia.query.Query;
54  import dev.morphia.query.Sort;
55  import dev.morphia.query.filters.Filters;
56  import dev.morphia.query.filters.RegexFilter;
57  
58  /**
59   * Basic {@link LazyDataModel} implementation for MongoDB using Morphia.
60   *
61   * @param <T> The model class.
62   */
63  public class MorphiaLazyDataModel<T> extends LazyDataModel<T> implements Serializable {
64  
65      private static final Logger LOGGER = Logger.getLogger(MorphiaLazyDataModel.class.getName());
66  
67      protected Class<T> entityClass;
68      protected Callbacks.SerializableSupplier<Datastore> datastore;
69      protected String rowKeyField;
70      protected Callbacks.SerializableFunction<T, Object> rowKeyProvider;
71  
72      /*
73       * if the default match mode queries in applyFilters() dont work for a specific field, overridden field queries with the overrideFieldQuery method will add
74       * BiConsumers to this map, where the key is the field name specified in <p:column filterBy="">
75       */
76      private final Map<String, BiConsumer<Query<T>, FilterMeta>> overrides = new HashMap<>();
77      // consumer to be executed before the query is built, useful to modify the original query
78      private transient Consumer<Query<T>> prependConsumer;
79      // global filter consumer (to be implemented by the user)
80      private transient BiConsumer<Query<T>, FilterMeta> globalFilterConsumer;
81      // for user supplied FindOptions
82      private transient Callbacks.SerializableSupplier<FindOptions> findOptionsSupplier;
83      // for user supplied CountOptions
84      private transient Callbacks.SerializableSupplier<CountOptions> countOptionsSupplier;
85  
86      /**
87       * For serialization only
88       */
89      public MorphiaLazyDataModel() {
90          // for serialization only
91      }
92  
93      @Override
94      public T getRowData(final String rowKey) {
95          List<T> values = Objects.requireNonNullElseGet(getWrappedData(), Collections::emptyList);
96          for (T obj : values) {
97              if (Objects.equals(rowKey, getRowKey(obj))) {
98                  return obj;
99              }
100         }
101 
102         return null;
103     }
104 
105     @Override
106     public String getRowKey(final T object) {
107         return String.valueOf(rowKeyProvider.apply(object));
108     }
109 
110     @Override
111     public int count(final Map<String, FilterMeta> map) {
112         final Query<T> q = this.buildQuery();
113         final CountOptions opts = getCountOptions();
114         final long count = applyFilters(q, map).count(opts);
115         return (int) count;
116     }
117 
118     @Override
119     public List<T> load(final int first, final int pageSize, final Map<String, SortMeta> sort,
120                 final Map<String, FilterMeta> filters) {
121         final Query<T> q = buildQuery();
122         final FindOptions opt = getFindOptions();
123         sort.forEach((field, sortData) -> opt.sort(sortData.getOrder() == SortOrder.DESCENDING ? Sort.descending(field) : Sort.ascending(field)));
124 
125         applyFilters(q, filters);
126         opt.skip(first).limit(pageSize);
127         try (MorphiaCursor<T> cursor = q.iterator(opt)) {
128             return cursor.toList();
129         }
130     }
131 
132     protected FindOptions getFindOptions() {
133         try {
134             return findOptionsSupplier != null ? findOptionsSupplier.get() : new FindOptions();
135         }
136         catch (Exception e) {
137             // if we get here, this means the user supplied FindOptions failed to resolve for some reason, so we fall back to the default
138             return new FindOptions();
139         }
140     }
141 
142     protected CountOptions getCountOptions() {
143         try {
144             return countOptionsSupplier != null ? countOptionsSupplier.get() : new CountOptions();
145         }
146         catch (Exception e) {
147             // if we get here, this means the user supplied CountOptions failed to resolve for some reason, so we fall back to the default
148             return new CountOptions();
149         }
150     }
151 
152     public Query<T> applyFilters(final Query<T> q, final Map<String, FilterMeta> filters) {
153         PrimeApplicationContext primeAppContext = PrimeApplicationContext.getCurrentInstance(FacesContext.getCurrentInstance());
154 
155         filters.forEach((field, metadata) -> {
156 
157             if (metadata.getFilterValue() != null) {
158                 final BiConsumer<Query<T>, FilterMeta> override = overrides.get(field);
159                 if (override != null) {
160                     override.accept(q, metadata);
161                 }
162                 else {
163                     final Object val = metadata.getFilterValue();
164                     if (metadata.getMatchMode() != null) {
165 
166                         switch (metadata.getMatchMode()) {
167                             case STARTS_WITH:
168                                 final RegexFilter regStartsWith = Filters.regex(field, "^" + val).caseInsensitive();
169                                 q.filter(regStartsWith);
170                                 break;
171                             case ENDS_WITH:
172                                 final RegexFilter regEndsWith = Filters.regex(field, val + "$").caseInsensitive();
173                                 q.filter(regEndsWith);
174                                 break;
175                             case CONTAINS:
176                                 q.filter(Filters.regex(field, val + "").caseInsensitive());
177                                 break;
178                             case EXACT:
179                                 final Object castedValueEx = convertToDataType(primeAppContext, field, val);
180                                 if (castedValueEx != null) {
181                                     q.filter(Filters.eq(field, castedValueEx));
182                                 }
183                                 else {
184                                     q.filter(Filters.eq(field, val));
185                                 }
186                                 break;
187                             case LESS_THAN:
188                                 final Object castedValueLt = convertToDataType(primeAppContext, field, val);
189                                 if (castedValueLt != null) {
190                                     q.filter(Filters.lt(field, castedValueLt));
191                                 }
192                                 else {
193                                     q.filter(Filters.lt(field, val));
194                                 }
195                                 break;
196                             case LESS_THAN_EQUALS:
197                                 final Object castedValueLte = convertToDataType(primeAppContext, field, val);
198                                 if (castedValueLte != null) {
199                                     q.filter(Filters.lte(field, castedValueLte));
200                                 }
201                                 else {
202                                     q.filter(Filters.lte(field, val));
203                                 }
204                                 break;
205                             case GREATER_THAN:
206                                 final Object castedValueGt = convertToDataType(primeAppContext, field, val);
207                                 if (castedValueGt != null) {
208                                     q.filter(Filters.gt(field, castedValueGt));
209                                 }
210                                 else {
211                                     q.filter(Filters.gt(field, val));
212                                 }
213                                 break;
214                             case GREATER_THAN_EQUALS:
215 
216                                 final Object castedValueGte = convertToDataType(primeAppContext, field, val);
217                                 if (castedValueGte != null) {
218                                     q.filter(Filters.gte(field, castedValueGte));
219                                 }
220                                 else {
221                                     q.filter(Filters.gte(field, val));
222                                 }
223                                 break;
224                             case EQUALS:
225                                 q.filter(Filters.eq(field, val));
226                                 break;
227                             case IN:
228                                 if (metadata.getFilterValue().getClass() == Object[].class) {
229                                     final Object[] parts = (Object[]) metadata.getFilterValue();
230                                     q.filter(Filters.in(field, Arrays.asList(parts)));
231                                 }
232                                 break;
233                             case BETWEEN:
234                                 if (metadata.getFilterValue() instanceof List) {
235                                     final List<?> dates = (List) metadata.getFilterValue();
236                                     if (dates.size() > 1) { // does this ever have less than 2 items?
237                                         q.filter(Filters.gte(field, dates.get(0)), Filters.lte(field, dates.get(1)));
238                                     }
239                                 }
240                                 break;
241                             case NOT_CONTAINS:
242                                 q.filter(Filters.regex(field, val + Constants.EMPTY_STRING).caseInsensitive().not());
243                                 break;
244                             case NOT_EQUALS:
245                                 final Object castedValueNe = convertToDataType(primeAppContext, field, val);
246                                 if (castedValueNe != null) {
247                                     q.filter(Filters.eq(field, castedValueNe).not());
248                                 }
249                                 else {
250                                     q.filter(Filters.eq(field, val).not());
251                                 }
252                                 break;
253                             case NOT_STARTS_WITH:
254                                 final RegexFilter regStartsWithNot = Filters.regex(field, "^" + val).caseInsensitive();
255                                 q.filter(regStartsWithNot.not());
256                                 break;
257                             case NOT_IN:
258                                 if (metadata.getFilterValue() instanceof Object[]) {
259                                     final Object[] parts = (Object[]) metadata.getFilterValue();
260                                     q.filter(Filters.nin(field, Arrays.asList(parts)));
261                                 }
262                                 break;
263                             case NOT_ENDS_WITH:
264                                 final RegexFilter regEndsWithNot = Filters.regex(field, val + "$").caseInsensitive();
265                                 q.filter(regEndsWithNot.not());
266                                 break;
267                             case GLOBAL:
268                                 if (globalFilterConsumer != null) {
269                                     globalFilterConsumer.accept(q, metadata);
270                                 }
271                                 break;
272 
273                             default:
274                                 throw new UnsupportedOperationException("MatchMode " + metadata.getMatchMode() + " not supported");
275                         }
276                     }
277                 }
278             }
279         });
280         return q;
281     }
282 
283     /**
284      * use {@link Builder#prependQuery(Consumer)} instead
285      */
286     @Deprecated
287     public MorphiaLazyDataModel<T> prependQuery(final Consumer<Query<T>> consumer) {
288         this.prependConsumer = consumer;
289         return this;
290     }
291 
292     /**
293      * use {@link Builder#findOptions(Callbacks.SerializableSupplier)} instead
294      */
295     @Deprecated
296     public MorphiaLazyDataModel<T> findOptions(final Callbacks.SerializableSupplier<FindOptions> supplier) {
297         this.findOptionsSupplier = supplier;
298         return this;
299     }
300 
301     /**
302      * use {@link Builder#findOptions(Callbacks.SerializableSupplier)} instead
303      */
304     @Deprecated
305     public MorphiaLazyDataModel<T> countOptions(final Callbacks.SerializableSupplier<CountOptions> supplier) {
306         this.countOptionsSupplier = supplier;
307         return this;
308     }
309 
310     /**
311      * use {@link Builder#globalFilter(BiConsumer)} instead
312      */
313     @Deprecated
314     public MorphiaLazyDataModel<T> globalFilter(final BiConsumer<Query<T>, FilterMeta> consumer) {
315         this.globalFilterConsumer = consumer;
316         return this;
317     }
318 
319     /**
320      * use {@link Builder#overrideFieldQuery(String, BiConsumer)} instead
321      */
322     @Deprecated
323     public MorphiaLazyDataModel<T> overrideFieldQuery(final String field, final BiConsumer<Query<T>, FilterMeta> consumer) {
324         this.overrides.put(field, consumer);
325         return this;
326     }
327 
328     private Object convertToDataType(PrimeApplicationContext primeAppContext, final String field, final Object value) {
329         PropertyDescriptorResolver propResolver = primeAppContext.getPropertyDescriptorResolver();
330         final PropertyDescriptor propertyDescriptor = propResolver.get(entityClass, field);
331         return ComponentUtils.convertToType(value, propertyDescriptor.getPropertyType(), LOGGER);
332     }
333 
334     private Query<T> buildQuery() {
335         final Query<T> q = datastore.get().find(entityClass).disableValidation();
336         if (prependConsumer != null) {
337             prependConsumer.accept(q);
338         }
339         return q;
340     }
341 
342     public static <T> Builder<T> builder() {
343         return new Builder<>();
344     }
345 
346     public static class Builder<T> {
347         private final MorphiaLazyDataModel<T> model;
348 
349         public Builder() {
350             model = new MorphiaLazyDataModel<>();
351         }
352 
353         public Builder<T> entityClass(Class<T> entityClass) {
354             model.entityClass = entityClass;
355             return this;
356         }
357 
358         public Builder<T> datastore(Callbacks.SerializableSupplier<Datastore> datastore) {
359             model.datastore = datastore;
360             return this;
361         }
362 
363         public Builder<T> rowKeyConverter(Converter<T> rowKeyConverter) {
364             model.rowKeyConverter = rowKeyConverter;
365             return this;
366         }
367 
368         public Builder<T> rowKeyProvider(Callbacks.SerializableFunction<T, Object> rowKeyProvider) {
369             model.rowKeyProvider = rowKeyProvider;
370             return this;
371         }
372 
373         public Builder<T> rowKeyField(String rowKey) {
374             model.rowKeyField = rowKey;
375             return this;
376         }
377 
378         public Builder<T> findOptions(Callbacks.SerializableSupplier<FindOptions> findOptionsSupplier) {
379             model.findOptionsSupplier = findOptionsSupplier;
380             return this;
381         }
382 
383         public Builder<T> countOptions(Callbacks.SerializableSupplier<CountOptions> countOptionsSupplier) {
384             model.countOptionsSupplier = countOptionsSupplier;
385             return this;
386         }
387 
388         public Builder<T> prependQuery(final Consumer<Query<T>> consumer) {
389             model.prependConsumer = consumer;
390             return this;
391         }
392 
393         public Builder<T> globalFilter(final BiConsumer<Query<T>, FilterMeta> consumer) {
394             model.globalFilterConsumer = consumer;
395             return this;
396         }
397 
398         public Builder<T> overrideFieldQuery(final String field, final BiConsumer<Query<T>, FilterMeta> consumer) {
399             model.overrides.put(field, consumer);
400             return this;
401         }
402 
403         public MorphiaLazyDataModel<T> build() {
404             Objects.requireNonNull(model.entityClass, "entityClass not set");
405             Objects.requireNonNull(model.datastore, "datastore not set");
406 
407             boolean requiresRowKeyProvider = model.rowKeyProvider == null && (model.rowKeyConverter != null || model.rowKeyField != null);
408             if (requiresRowKeyProvider) {
409                 if (model.rowKeyConverter != null) {
410                     model.rowKeyProvider = model::getRowKeyFromConverter;
411                 }
412                 else {
413                     Objects.requireNonNull(model.rowKeyField, "rowKeyField is mandatory if neither rowKeyProvider nor converter is provided");
414 
415                     PropertyDescriptorResolver propResolver = PrimeApplicationContext.getCurrentInstance(FacesContext.getCurrentInstance())
416                                 .getPropertyDescriptorResolver();
417                     model.rowKeyProvider = obj -> propResolver.getValue(obj, model.rowKeyField);
418                 }
419             }
420             return model;
421         }
422     }
423 }