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<Person> <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> -> <jv>p</jv>.getName().substring(0, 1)) 052 * .guard(Person::isActive) 053 * .build(); 054 * 055 * <jk>try</jk> (InMemoryBeanQuerySession<Person> <jv>session</jv> = <jv>context</jv>.getSession(<jv>people</jv>)) { 056 * Page<Person> <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<Person> <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<Map<String,Object>> <jv>maps</jv> = InMemoryBeanQueryContext 066 * .<Map<String,Object>><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}