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&lt;Person&gt; <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> -&gt; <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}