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.*; 022 023/** 024 * One page of query results, plus the optional counts selected by the session's {@link CountPolicy}. 025 * 026 * <ul> 027 * <li>{@link #total()}: rows that pass the guards (before the search). 028 * <li>{@link #matched()}: rows that pass the guards <b>and</b> the search. 029 * </ul> 030 * 031 * <p> 032 * Under {@link CountPolicy#NONE} neither count is present. Under {@link CountPolicy#MATCHED} only 033 * {@link #matched()} is. Under {@link CountPolicy#BOTH} both are. Counts are opt-in and independent of each 034 * other: a caller that did not ask for a count gets an empty {@link OptionalLong} back rather than an exception, so 035 * "was a count computed" and "read the count" are the same call. 036 * 037 * <h5 class='section'>Example:</h5> 038 * <p class='bjava'> 039 * Page<Person> <jv>page</jv> = <jv>session</jv>.find(BeanQuery.<jsm>create</jsm>().counts(CountRequest.<jsf>BOTH</jsf>).build()); 040 * <jk>for</jk> (Person <jv>p</jv> : <jv>page</jv>.rows()) 041 * <jsm>render</jsm>(<jv>p</jv>); 042 * <jv>page</jv>.total().ifPresent(<jv>total</jv> -> <jsm>renderFooter</jsm>(<jv>page</jv>.matched().getAsLong() + <js>" of "</js> + <jv>total</jv>)); 043 * </p> 044 * 045 * @param <T> The row type. 046 * @since 10.0.0 047 */ 048public final class Page<T> { 049 050 private final List<T> rows; 051 private final Long total; 052 private final Long matched; 053 054 private Page(List<T> rows, Long total, Long matched) { 055 this.rows = copyRows(rows); 056 this.total = total; 057 this.matched = matched; 058 } 059 060 // List.copyOf throws a bare, message-less NullPointerException on a null element; give it a clear cause instead. 061 private static <T> List<T> copyRows(List<T> rows) { 062 for (var i = 0; i < rows.size(); i++) 063 if (rows.get(i) == null) 064 throw new BeanQueryExecutionException(null, "Row %s is null.", i); 065 return List.copyOf(rows); 066 } 067 068 /** 069 * Creates a page without counts. 070 * 071 * @param <T> The row type. 072 * @param rows The rows. Must not be <jk>null</jk> or contain <jk>null</jk>. 073 * @return A new page. 074 */ 075 public static <T> Page<T> of(List<T> rows) { 076 return new Page<>(rows, null, null); 077 } 078 079 /** 080 * Creates a page with both counts. 081 * 082 * @param <T> The row type. 083 * @param rows The rows. Must not be <jk>null</jk> or contain <jk>null</jk>. 084 * @param total The number of rows that pass the guards. 085 * @param matched The number of rows that pass the guards and the search. 086 * @return A new page. 087 */ 088 public static <T> Page<T> of(List<T> rows, long total, long matched) { 089 return new Page<>(rows, total, matched); 090 } 091 092 /** 093 * Creates a page with only the matched count. 094 * 095 * @param <T> The row type. 096 * @param rows The rows. Must not be <jk>null</jk> or contain <jk>null</jk>. 097 * @param matched The number of rows that pass the guards and the search. 098 * @return A new page. 099 */ 100 public static <T> Page<T> ofMatched(List<T> rows, long matched) { 101 return new Page<>(rows, null, matched); 102 } 103 104 /** 105 * Returns the rows of this page. 106 * 107 * @return An unmodifiable list (never <jk>null</jk>). 108 */ 109 public List<T> rows() { 110 return rows; 111 } 112 113 /** 114 * The number of rows in scope before the query's search (after any server-only guard). 115 * 116 * @return The total row count, or empty if it was not requested. 117 */ 118 public OptionalLong total() { 119 return total == null ? OptionalLong.empty() : OptionalLong.of(total); 120 } 121 122 /** 123 * The number of rows that survive the search, before any page limit. 124 * 125 * @return The matched row count, or empty if it was not requested. 126 */ 127 public OptionalLong matched() { 128 return matched == null ? OptionalLong.empty() : OptionalLong.of(matched); 129 } 130 131 @Override /* Object */ 132 public boolean equals(Object o) { 133 return this == o || (o instanceof Page<?> p && eq(rows, p.rows) && eq(total, p.total) && eq(matched, p.matched)); 134 } 135 136 @Override /* Object */ 137 public int hashCode() { 138 return h(rows, total, matched); 139 } 140 141 @Override /* Object */ 142 public String toString() { 143 var sb = new StringBuilder("Page[rows=").append(rows.size()); 144 if (total != null) 145 sb.append(", total=").append(total); 146 if (matched != null) 147 sb.append(", matched=").append(matched); 148 return sb.append(']').toString(); 149 } 150}