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.*;
023
024import org.apache.juneau.commons.bean.*;
025
026/**
027 * The in-memory {@link BeanQueryContext}: runs {@link BeanQuery}s against an in-JVM collection of beans or maps.
028 *
029 * <p>
030 * {@link #create(Class)} declares one column per readable Juneau bean property: getters ({@code getX()}, or
031 * {@code isX()} for booleans), public fields, and record components, named and filtered by the same rules serializers
032 * use ({@link BeanIgnore @BeanIgnore}, {@link BeanProp @BeanProp}, the property namer).  The value type is inferred
033 * from the property type.  Use {@link #create(Class, BeanConfigContext)} or {@link #create(BeanMeta)} to apply a
034 * specific bean configuration.  Nothing else is queryable: {@code class}, {@code toString} and other methods are not
035 * columns.  Narrow the list with {@link Builder#exclude(String...) exclude} or
036 * {@link Builder#columns(String...) columns}, add derived columns with {@link Builder#column(String, SearchType)
037 * column} plus {@link Builder#accessor(String, Function) accessor}, and scope every session with a server-only
038 * {@link Builder#guard(Predicate) guard}.  {@link #create()} is for {@code Map} rows: declare each column, and it is
039 * read by key.
040 *
041 * <p>
042 * The context holds no rows.  Open a session over a specific collection with {@link #getSession(Collection)} or
043 * {@link #createSession()}.
044 *
045 * <h5 class='section'>Example:</h5>
046 * <p class='bjava'>
047 *    InMemoryBeanQueryContext&lt;Person&gt; <jv>context</jv> = InMemoryBeanQueryContext
048 *       .<jsm>create</jsm>(Person.<jk>class</jk>)
049 *       .exclude(<js>"ssn"</js>)
050 *       .column(<js>"initial"</js>, SearchType.<jsf>TEXT</jsf>)
051 *       .accessor(<js>"initial"</js>, <jv>p</jv> -&gt; <jv>p</jv>.getName().substring(0, 1))
052 *       .guard(Person::isActive)
053 *       .build();
054 *
055 *    <jk>try</jk> (InMemoryBeanQuerySession&lt;Person&gt; <jv>session</jv> = <jv>context</jv>.getSession(<jv>people</jv>)) {
056 *       Page&lt;Person&gt; <jv>page</jv> = <jv>session</jv>.find(BeanQuery.<jsm>create</jsm>().setSearch(<js>"age=$gt(21)"</js>).setSort(<js>"name"</js>));
057 *    }
058 *
059 *    <jc>// The bean rules of a specific serializer configuration.</jc>
060 *    InMemoryBeanQueryContext&lt;Person&gt; <jv>dashed</jv> = InMemoryBeanQueryContext
061 *       .<jsm>create</jsm>(Person.<jk>class</jk>, BeanConfigContext.<jsm>create</jsm>().propertyNamer(PropertyNamerDLC.<jsf>INSTANCE</jsf>).build())
062 *       .build();
063 *
064 *    <jc>// Map rows: declare the columns.</jc>
065 *    InMemoryBeanQueryContext&lt;Map&lt;String,Object&gt;&gt; <jv>maps</jv> = InMemoryBeanQueryContext
066 *       .&lt;Map&lt;String,Object&gt;&gt;<jsm>create</jsm>()
067 *       .column(<js>"name"</js>, SearchType.<jsf>TEXT</jsf>)
068 *       .column(<js>"age"</js>, SearchType.<jsf>NUMERIC</jsf>)
069 *       .build();
070 * </p>
071 *
072 * @param <T> The row (bean) type.
073 * @since 10.0.0
074 */
075public final class InMemoryBeanQueryContext<T> extends BeanQueryContext<T> {
076
077   /**
078    * Builder for {@link InMemoryBeanQueryContext}.  See the class Javadoc for an example.
079    *
080    * @param <T> The row type.
081    */
082   public static final class Builder<T> extends BeanQueryContext.Builder<T,Builder<T>> {
083
084      private final Class<T> beanClass;
085      private final QueryModel<T> model;  // Null for Map rows.
086      private final Map<String,Function<? super T,?>> accessors = new LinkedHashMap<>();
087      private Predicate<? super T> guard;
088
089      Builder(QueryModel<T> model) {
090         this.model = model;
091         beanClass = model == null ? null : model.getBeanClass();
092         if (model != null)
093            model.getReadableProperties().forEach((name, p) -> settings.columns.put(name, p.getSearchType()));
094      }
095
096      Builder(InMemoryBeanQueryContext<T> copyFrom) {
097         super(copyFrom);
098         beanClass = copyFrom.beanClass;
099         model = copyFrom.model;
100         accessors.putAll(copyFrom.accessors);
101         guard = copyFrom.guard;
102      }
103
104      /**
105       * Sets how a declared column's value is read, replacing the bean property (or map key).
106       *
107       * @param column The column name.  Must be declared.
108       * @param value The accessor.  Must not be <jk>null</jk>.  Exceptions it throws become
109       *    {@link BeanQueryExecutionException}s.
110       * @return This object.
111       */
112      public Builder<T> accessor(String column, Function<? super T,?> value) {
113         accessors.put(declared(column), reqnn("value", value));
114         return this;
115      }
116
117      /**
118       * Sets a server-only filter applied to every session before the search.  Rows it rejects are not counted in
119       * {@link Page#total()}.
120       *
121       * @param value The guard, or <jk>null</jk> for none.
122       * @return This object.
123       */
124      public Builder<T> guard(Predicate<? super T> value) {
125         guard = value;
126         return this;
127      }
128
129      /**
130       * {@inheritDoc}
131       *
132       * @throws IllegalArgumentException If a bean column has neither a readable property nor an accessor.
133       */
134      @Override /* BeanQueryContext.Builder */
135      public InMemoryBeanQueryContext<T> build() {
136         return new InMemoryBeanQueryContext<>(this);
137      }
138   }
139
140   /**
141    * Returns a builder whose columns are the bean's readable properties, per {@link BeanConfigContext#DEFAULT}.
142    *
143    * @param <T> The bean type.
144    * @param beanClass The bean class.  Must not be <jk>null</jk>.
145    * @return A new builder.
146    */
147   public static <T> Builder<T> create(Class<T> beanClass) {
148      return new Builder<>(QueryModel.of(reqnn("beanClass", beanClass)));
149   }
150
151   /**
152    * Returns a builder whose columns are the bean's readable properties under the given bean configuration.
153    *
154    * <p>
155    * Pass the configuration your serializer uses so column names match the property names it emits.
156    *
157    * @param <T> The bean type.
158    * @param beanClass The bean class.  Must not be <jk>null</jk>.
159    * @param config The bean configuration.  Must not be <jk>null</jk>.
160    * @return A new builder.
161    */
162   public static <T> Builder<T> create(Class<T> beanClass, BeanConfigContext config) {
163      return new Builder<>(QueryModel.of(reqnn("beanClass", beanClass), reqnn("config", config)));
164   }
165
166   /**
167    * Returns a builder whose columns are the readable properties of existing bean metadata.
168    *
169    * @param <T> The bean type.
170    * @param beanMeta The bean metadata.  Must not be <jk>null</jk>.
171    * @return A new builder.
172    */
173   public static <T> Builder<T> create(BeanMeta<T> beanMeta) {
174      return new Builder<>(QueryModel.of(reqnn("beanMeta", beanMeta)));
175   }
176
177   /**
178    * Returns a builder for {@code Map} rows.  It has no columns until you declare them.
179    *
180    * @param <T> The row type, usually {@code Map<String,Object>}.
181    * @return A new builder.
182    */
183   public static <T> Builder<T> create() {
184      return new Builder<>((QueryModel<T>)null);
185   }
186
187   private final Class<T> beanClass;
188   private final QueryModel<T> model;
189   private final Map<String,Function<? super T,?>> accessors;
190   private final Map<String,Function<? super T,?>> readers;
191   private final Predicate<? super T> guard;
192
193   InMemoryBeanQueryContext(Builder<T> builder) {
194      super(builder);
195      beanClass = builder.beanClass;
196      model = builder.model;
197      accessors = Collections.unmodifiableMap(new LinkedHashMap<>(builder.accessors));
198      guard = builder.guard;
199      var m = new LinkedHashMap<String,Function<? super T,?>>();
200      for (var c : settings.columns.keySet())
201         m.put(c, reader(c));
202      readers = Collections.unmodifiableMap(m);
203   }
204
205   private Function<? super T,?> reader(String column) {
206      var a = accessors.get(column);
207      if (a != null)
208         return a;
209      var p = model == null ? null : model.getReadableProperty(column);
210      if (p != null)
211         return p::read;
212      if (beanClass == null)
213         return row -> ((Map<?,?>)row).get(column);
214      throw iaex("Column '%s' is not a readable bean property of %s; declare an accessor.", column, beanClass.getName());
215   }
216
217   /**
218    * Reads one cell.
219    *
220    * @param row The row.
221    * @param column A declared column.
222    * @return The value.  Can be <jk>null</jk>.
223    * @throws BeanQueryExecutionException If the accessor or property read fails.
224    */
225   Object cell(T row, String column) {
226      try {
227         return readers.get(column).apply(row);
228      } catch (BeanQueryExecutionException | BeanQuerySyntaxException e) {
229         throw e;
230      } catch (RuntimeException e) {
231         throw new BeanQueryExecutionException(e, "Failed to read column '%s'.", column);
232      }
233   }
234
235   /**
236    * Tests the context guard.
237    *
238    * @param row The row.
239    * @return <jk>true</jk> if there is no guard or the guard accepts the row.
240    */
241   boolean inScope(T row) {
242      return guard == null || guard.test(row);
243   }
244
245   @Override /* BeanQueryContext */
246   public Builder<T> copy() {
247      return new Builder<>(this);
248   }
249
250   @Override /* BeanQueryContext */
251   public InMemoryBeanQuerySession.Builder<T> createSession() {
252      return new InMemoryBeanQuerySession.Builder<>(this);
253   }
254
255   /**
256    * Opens a session over a collection with this context's settings as-is.
257    *
258    * @param rows The rows.  Must not be <jk>null</jk>.
259    * @return A new session.
260    */
261   @SuppressWarnings({
262      "resource" // Caller takes ownership of the returned session
263   })
264   public InMemoryBeanQuerySession<T> getSession(Collection<? extends T> rows) {
265      return createSession().rows(rows).build();
266   }
267}