001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.beanquery;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022import java.util.function.*;
023import java.util.stream.*;
024
025/**
026 * A {@link BeanQuerySession} over one in-memory collection.
027 *
028 * <p>
029 * The rows are filtered once, when the session is built, through the context guard <b>and</b> the session guard.
030 * Every call then searches, sorts and pages that scoped list.  All {@code $regex} matching in one call shares one
031 * deadline ({@link BeanQueryContext.Builder#regexTimeout(java.time.Duration) regexTimeout}).  {@link #close()} is a
032 * no-op, but use try-with-resources anyway so the code does not change if the engine does.
033 *
034 * <h5 class='section'>Example:</h5>
035 * <p class='bjava'>
036 *    <jk>try</jk> (InMemoryBeanQuerySession&lt;Person&gt; <jv>session</jv> = <jv>context</jv>.createSession()
037 *          .rows(<jv>people</jv>)
038 *          .guard(<jv>p</jv> -&gt; <jv>p</jv>.getTenant().equals(<jv>tenant</jv>))   <jc>// This request's tenant only.</jc>
039 *          .build()) {
040 *       Page&lt;Person&gt; <jv>page</jv> = <jv>session</jv>.find(<jv>query</jv>);
041 *       Counts <jv>counts</jv> = <jv>session</jv>.count(<jv>query</jv>);
042 *    }
043 * </p>
044 *
045 * @param <T> The row (bean) type.
046 * @since 10.0.0
047 */
048public final class InMemoryBeanQuerySession<T> extends BeanQuerySession<T> {
049
050   /**
051    * Builder for {@link InMemoryBeanQuerySession}.  See the class Javadoc for an example.
052    *
053    * @param <T> The row type.
054    */
055   public static final class Builder<T> extends BeanQuerySession.Builder<T,Builder<T>> {
056
057      private final InMemoryBeanQueryContext<T> context;
058      private Collection<? extends T> rows;
059      private Predicate<? super T> guard;
060
061      Builder(InMemoryBeanQueryContext<T> context) {
062         super(context);
063         this.context = context;
064      }
065
066      /**
067       * Sets the rows to query.  Required.
068       *
069       * @param value The rows.  Must not be <jk>null</jk>.
070       * @return This object.
071       */
072      public Builder<T> rows(Collection<? extends T> value) {
073         rows = reqnn("value", value);
074         return this;
075      }
076
077      /**
078       * Sets a per-request filter, applied together with the context guard.
079       *
080       * @param value The guard, or <jk>null</jk> for none.
081       * @return This object.
082       */
083      public Builder<T> guard(Predicate<? super T> value) {
084         guard = value;
085         return this;
086      }
087
088      /**
089       * {@inheritDoc}
090       *
091       * @throws IllegalStateException If no rows were set.
092       */
093      @Override /* BeanQuerySession.Builder */
094      @SuppressWarnings({
095         "resource" // Caller takes ownership of the returned session
096      })
097      public InMemoryBeanQuerySession<T> build() {
098         if (rows == null)
099            throw isex("InMemoryBeanQuerySession requires rows.");
100         return new InMemoryBeanQuerySession<>(this);
101      }
102   }
103
104   private final InMemoryBeanQueryContext<T> context;
105   private final List<T> scoped;
106
107   @SuppressWarnings({
108      "java:S9391" // Per-row try/catch that re-wraps guard failures; a stream lambda would obscure it.
109   })
110   InMemoryBeanQuerySession(Builder<T> builder) {
111      super(builder);
112      context = builder.context;
113      var g = builder.guard;
114      var l = new ArrayList<T>();
115      for (T r : builder.rows) {
116         try {
117            if (context.inScope(r) && (g == null || g.test(r)))
118               l.add(r);
119         } catch (BeanQueryExecutionException | BeanQuerySyntaxException e) {
120            throw e;
121         } catch (RuntimeException e) {
122            throw new BeanQueryExecutionException(e, "Failed to apply guard.");
123         }
124      }
125      scoped = Collections.unmodifiableList(l);
126   }
127
128   /**
129    * {@inheritDoc}
130    *
131    * @return The context that created this session.
132    */
133   @Override /* BeanQuerySession */
134   public InMemoryBeanQueryContext<T> getContext() {
135      return context;
136   }
137
138   @Override /* BeanQuerySession */
139   protected Page<T> doFind(ResolvedQuery query) {
140      var m = matched(query);
141      return page(slice(sorted(m, query), query), query, scoped::size, m::size);
142   }
143
144   @Override /* BeanQuerySession */
145   protected Page<Map<String,Object>> doFindValues(ResolvedQuery query) {
146      var m = matched(query);
147      return page(values(slice(sorted(m, query), query), query), query, scoped::size, m::size);
148   }
149
150   @Override /* BeanQuerySession */
151   protected Counts doCount(ResolvedQuery query) {
152      return Counts.of(scoped.size(), matched(query).size());
153   }
154
155   @Override /* BeanQuerySession */
156   protected Stream<T> doStream(ResolvedQuery query) {
157      return slice(sorted(matched(query), query), query).stream();
158   }
159
160   @Override /* BeanQuerySession */
161   protected Stream<Map<String,Object>> doStreamValues(ResolvedQuery query) {
162      return values(slice(sorted(matched(query), query), query), query).stream();
163   }
164
165   @Override /* BeanQuerySession */
166   public void close() {
167      // Nothing to release: the rows belong to the caller.
168   }
169
170   private List<T> matched(ResolvedQuery query) {
171      var filter = query.filter();
172      if (filter instanceof Filter.And and && and.items().isEmpty())
173         return scoped;  // No search: every scoped row matches, without constructing a RegexBudget for nothing.
174      var budget = new RegexBudget(settings.regexTimeout);  // One deadline for the whole call.
175      return scoped.stream().filter(r -> FilterEvaluator.matches(filter, col -> context.cell(r, col), budget)).toList();
176   }
177
178   private List<T> sorted(List<T> rows, ResolvedQuery query) {
179      Comparator<T> c = null;
180      for (var k : query.sort()) {
181         Comparator<T> next = (a, b) -> InMemoryMatch.compareCells(context.cell(a, k.column()), context.cell(b, k.column()), k.type());
182         if (k.descending())
183            next = next.reversed();
184         c = c == null ? next : c.thenComparing(next);
185      }
186      if (c == null)
187         return rows;
188      var l = new ArrayList<>(rows);
189      l.sort(c);
190      return l;
191   }
192
193   private static <R> List<R> slice(List<R> rows, ResolvedQuery query) {
194      var from = Math.min(query.position(), rows.size());
195      var limit = query.limit();
196      var to = limit == null ? rows.size() : (int)Math.min((long)from + limit, rows.size());
197      return rows.subList(from, to);
198   }
199
200   private List<Map<String,Object>> values(List<T> rows, ResolvedQuery query) {
201      var l = new ArrayList<Map<String,Object>>(rows.size());
202      for (var r : rows) {
203         var m = new LinkedHashMap<String,Object>();
204         for (var c : query.view())
205            m.put(c, context.cell(r, c));
206         l.add(Collections.unmodifiableMap(m));
207      }
208      return l;
209   }
210}