001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.utils;
014
015import static java.util.Collections.*;
016import static org.apache.juneau.internal.StringUtils.*;
017
018import java.util.*;
019
020import org.apache.juneau.*;
021import org.apache.juneau.internal.*;
022
023/**
024 * Encapsulates arguments for basic search/view/sort/position/limit functionality.
025 */
026public class SearchArgs {
027
028   /**
029    * Default search args.
030    */
031   public static SearchArgs DEFAULT = SearchArgs.builder().build();
032
033   private final Map<String,String> search;
034   private final List<String> view;
035   private final Map<String,Boolean> sort;
036   private final int position, limit;
037   private final boolean ignoreCase;
038
039   SearchArgs(Builder b) {
040      this.search = unmodifiableMap(new LinkedHashMap<>(b.search));
041      this.view = unmodifiableList(new ArrayList<>(b.view));
042      this.sort = unmodifiableMap(new LinkedHashMap<>(b.sort));
043      this.position = b.position;
044      this.limit = b.limit;
045      this.ignoreCase = b.ignoreCase;
046   }
047
048   /**
049    * Creates a new builder for {@link SearchArgs}
050    *
051    * @return A new builder for {@link SearchArgs}
052    */
053   public static Builder builder() {
054      return new Builder();
055   }
056
057   /**
058    * Builder for {@link SearchArgs} class.
059    */
060   public static final class Builder {
061      Map<String,String> search = new LinkedHashMap<>();
062      List<String> view = new ArrayList<>();
063      Map<String,Boolean> sort = new LinkedHashMap<>();
064      int position, limit;
065      boolean ignoreCase;
066
067      /**
068       * Adds search terms to this builder.
069       *
070       * <p>
071       * The search terms are a comma-delimited list of key/value pairs of column-names and search tokens.
072       *
073       * <p>
074       * For example:
075       * <p class='bcode w800'>
076       *    builder.search(<js>"column1=foo*, column2=bar baz"</js>);
077       * </p>
078       *
079       * <p>
080       * It's up to implementers to decide the syntax and meaning of the search terms.
081       *
082       * <p>
083       * Whitespace is trimmed from column names and search tokens.
084       *
085       * @param searchTerms
086       *    The search terms string.
087       *    Can be <jk>null</jk>.
088       * @return This object (for method chaining).
089       */
090      public Builder search(String searchTerms) {
091         if (searchTerms != null) {
092            for (String s : StringUtils.split(searchTerms)) {
093               int i = StringUtils.indexOf(s, '=', '>', '<');
094               if (i == -1)
095                  throw new FormattedRuntimeException("Invalid search terms: ''{0}''", searchTerms);
096               char c = s.charAt(i);
097               search(s.substring(0, i).trim(), s.substring(c == '=' ? i+1 : i).trim());
098            }
099         }
100         return this;
101      }
102
103      /**
104       * Adds a search term to this builder.
105       *
106       * <p>
107       * It's up to implementers to decide the syntax and meaning of the search term.
108       *
109       * @param column The column being searched.
110       * @param searchTerm The search term.
111       * @return This object (for method chaining).
112       */
113      public Builder search(String column, String searchTerm) {
114         this.search.put(column, searchTerm);
115         return this;
116      }
117
118      /**
119       * Specifies the list of columns to view.
120       *
121       * <p>
122       * The columns argument is a simple comma-delimited list of column names.
123       *
124       * <p>
125       * For example:
126       * <p class='bcode w800'>
127       *    builder.view(<js>"column1, column2"</js>);
128       * </p>
129       *
130       * <p>
131       * Whitespace is trimmed from column names.
132       *
133       * <p>
134       * Empty view columns imply view all columns.
135       *
136       * @param columns
137       *    The columns being viewed.
138       *    Can be <jk>null</jk>.
139       * @return This object (for method chaining).
140       */
141      public Builder view(String columns) {
142         if (columns != null)
143            return view(Arrays.asList(StringUtils.split(columns)));
144         return this;
145      }
146
147      /**
148       * Specifies the list of columns to view.
149       *
150       * <p>
151       * Empty view columns imply view all columns.
152       *
153       * @param columns The columns being viewed.
154       * @return This object (for method chaining).
155       */
156      public Builder view(Collection<String> columns) {
157         this.view.addAll(columns);
158         return this;
159      }
160
161      /**
162       * Specifies the sort arguments.
163       *
164       * <p>
165       * The sort argument is a simple comma-delimited list of column names.
166       * <br>Column names can be suffixed with <js>'+'</js> or <js>'-'</js> to indicate ascending or descending order.
167       * <br>No suffix implies ascending order.
168       *
169       * <p>
170       * For example:
171       * <p class='bcode w800'>
172       *    <jc>// Order by column1 ascending, then column2 descending.</jc>
173       *    builder.sort(<js>"column1, column2-"</js>);
174       * </p>
175       *
176       * <p>
177       * Note that the order of the order arguments is important.
178       *
179       * <p>
180       * Whitespace is trimmed from column names.
181       *
182       * @param sortArgs
183       *    The columns to sort by.
184       *    Can be <jk>null</jk>.
185       * @return This object (for method chaining).
186       */
187      public Builder sort(String sortArgs) {
188         if (sortArgs != null)
189            sort(Arrays.asList(StringUtils.split(sortArgs)));
190         return this;
191      }
192
193      /**
194       * Specifies the sort arguments.
195       *
196       * <p>
197       * Column names can be suffixed with <js>'+'</js> or <js>'-'</js> to indicate ascending or descending order.
198       * <br>No suffix implies ascending order.
199       *
200       * <p>
201       * Note that the order of the sort is important.
202       *
203       * @param sortArgs
204       *    The columns to sort by.
205       *    Can be <jk>null</jk>.
206       * @return This object (for method chaining).
207       */
208      public Builder sort(Collection<String> sortArgs) {
209         for (String s : sortArgs) {
210            boolean isDesc = false;
211            if (endsWith(s, '-', '+')) {
212               isDesc = endsWith(s, '-');
213               s = s.substring(0, s.length()-1);
214            }
215            this.sort.put(s, isDesc);
216         }
217         return this;
218      }
219
220      /**
221       * Specifies the starting line number.
222       *
223       * @param position The zero-indexed position.
224       * @return This object (for method chaining).
225       */
226      public Builder position(int position) {
227         this.position = position;
228         return this;
229      }
230
231      /**
232       * Specifies the number of rows to return.
233       *
234       * @param limit
235       *    The number of rows to return.
236       *    If <code>&lt;=0</code>, all rows should be returned.
237       * @return This object (for method chaining).
238       */
239      public Builder limit(int limit) {
240         this.limit = limit;
241         return this;
242      }
243
244      /**
245       * Specifies whether case-insensitive search should be used.
246       *
247       * <p>
248       * The default is <jk>false</jk>.
249       *
250       * @param value The ignore-case flag value.
251       * @return This object (for method chaining).
252       */
253      public Builder ignoreCase(boolean value) {
254         this.ignoreCase = value;
255         return this;
256      }
257
258      /**
259       * Construct the {@link SearchArgs} object.
260       *
261       * <p>
262       * This method can be called multiple times to construct new objects.
263       *
264       * @return A new {@link SearchArgs} object initialized with values in this builder.
265       */
266      public SearchArgs build() {
267         return new SearchArgs(this);
268      }
269   }
270
271   /**
272    * The query search terms.
273    *
274    * <p>
275    * The search terms are key/value pairs consisting of column-names and search tokens.
276    *
277    * <p>
278    * It's up to implementers to decide the syntax and meaning of the search term.
279    *
280    * @return An unmodifiable map of query search terms.
281    */
282   public Map<String,String> getSearch() {
283      return search;
284   }
285
286   /**
287    * The view columns.
288    *
289    * <p>
290    * The view columns are the list of columns that should be displayed.
291    * An empty list implies all columns should be displayed.
292    *
293    * @return An unmodifiable list of columns to view.
294    */
295   public List<String> getView() {
296      return view;
297   }
298
299   /**
300    * The sort columns.
301    *
302    * <p>
303    * The sort columns are key/value pairs consisting of column-names and direction flags
304    * (<jk>false</jk> = ascending, <jk>true</jk> = descending).
305    *
306    * @return An unmodifiable ordered map of sort columns and directions.
307    */
308   public Map<String,Boolean> getSort() {
309      return sort;
310   }
311
312   /**
313    * The first-row position.
314    *
315    * @return
316    *    The zero-indexed row number of the first row to display.
317    *    Default is <code>0</code>
318    */
319   public int getPosition() {
320      return position;
321   }
322
323   /**
324    * The number of rows to return.
325    *
326    * @return
327    *    The number of rows to return in the result.
328    *    Default is <code>0</code> which means return all rows.
329    */
330   public int getLimit() {
331      return limit;
332   }
333
334   /**
335    * The ignore-case flag.
336    *
337    * <p>
338    * Used in conjunction with {@link #getSearch()} to specify whether case-insensitive searches should be performed.
339    *
340    * @return
341    *    The number of rows to return in the result.
342    *    Default is <jk>false</jk>.
343    */
344   public boolean isIgnoreCase() {
345      return ignoreCase;
346   }
347}