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<Person> <jv>session</jv> = <jv>context</jv>.createSession() 037 * .rows(<jv>people</jv>) 038 * .guard(<jv>p</jv> -> <jv>p</jv>.getTenant().equals(<jv>tenant</jv>)) <jc>// This request's tenant only.</jc> 039 * .build()) { 040 * Page<Person> <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}