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