001/*
002 * Copyright 2015-2023 Ping Identity Corporation
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018package com.unboundid.scim2.client.requests;
019
020import com.fasterxml.jackson.core.JsonParser;
021import com.fasterxml.jackson.core.JsonToken;
022import com.fasterxml.jackson.databind.node.ObjectNode;
023import com.unboundid.scim2.client.ScimService;
024import com.unboundid.scim2.client.SearchResultHandler;
025import com.unboundid.scim2.common.ScimResource;
026import com.unboundid.scim2.common.exceptions.ScimException;
027import com.unboundid.scim2.common.messages.ListResponse;
028import com.unboundid.scim2.common.messages.SearchRequest;
029import com.unboundid.scim2.common.messages.SortOrder;
030import com.unboundid.scim2.common.utils.ApiConstants;
031import com.unboundid.scim2.common.utils.JsonUtils;
032import com.unboundid.scim2.common.utils.SchemaUtils;
033import com.unboundid.scim2.common.utils.StaticUtils;
034
035import javax.ws.rs.client.Entity;
036import javax.ws.rs.client.Invocation;
037import javax.ws.rs.client.ResponseProcessingException;
038import javax.ws.rs.client.WebTarget;
039import javax.ws.rs.core.MediaType;
040import javax.ws.rs.core.Response;
041import java.io.IOException;
042import java.io.InputStream;
043import java.util.List;
044import java.util.Map;
045import java.util.Set;
046
047import static com.unboundid.scim2.common.utils.ApiConstants.QUERY_PARAMETER_FILTER;
048import static com.unboundid.scim2.common.utils.ApiConstants.QUERY_PARAMETER_PAGE_SIZE;
049import static com.unboundid.scim2.common.utils.ApiConstants.QUERY_PARAMETER_PAGE_START_INDEX;
050import static com.unboundid.scim2.common.utils.ApiConstants.QUERY_PARAMETER_SORT_BY;
051import static com.unboundid.scim2.common.utils.ApiConstants.QUERY_PARAMETER_SORT_ORDER;
052
053/**
054 * A builder for SCIM search requests.
055 */
056public final class SearchRequestBuilder
057    extends ResourceReturningRequestBuilder<SearchRequestBuilder>
058{
059  private String filter;
060  private String sortBy;
061  private SortOrder sortOrder;
062  private Integer startIndex;
063  private Integer count;
064
065  /**
066   * Create a new  search request builder.
067   *
068   * @param target The WebTarget to search.
069   */
070  public SearchRequestBuilder(final WebTarget target)
071  {
072    super(target);
073  }
074
075  /**
076   * Request filtering of resources.
077   *
078   * @param filter the filter string used to request a subset of resources.
079   * @return This builder.
080   */
081  public SearchRequestBuilder filter(final String filter)
082  {
083    this.filter = filter;
084    return this;
085  }
086
087  /**
088   * Request sorting of resources.
089   *
090   * @param sortBy the string indicating the attribute whose value shall be used
091   *               to order the returned responses.
092   * @param sortOrder the order in which the sortBy parameter is applied.
093   * @return This builder.
094   */
095  public SearchRequestBuilder sort(final String sortBy,
096                                   final SortOrder sortOrder)
097  {
098    this.sortBy = sortBy;
099    this.sortOrder = sortOrder;
100    return this;
101  }
102
103  /**
104   * Request pagination of resources.
105   *
106   * @param startIndex the 1-based index of the first query result.
107   * @param count the desired maximum number of query results per page.
108   * @return This builder.
109   */
110  public SearchRequestBuilder page(final int startIndex,
111                                   final int count)
112  {
113    this.startIndex = startIndex;
114    this.count = count;
115    return this;
116  }
117
118  /**
119   * {@inheritDoc}
120   */
121  @Override
122  WebTarget buildTarget()
123  {
124    WebTarget target = super.buildTarget();
125    if(filter != null)
126    {
127      target = target.queryParam(QUERY_PARAMETER_FILTER, filter);
128    }
129    if(sortBy != null && sortOrder != null)
130    {
131      target = target.queryParam(QUERY_PARAMETER_SORT_BY, sortBy);
132      target = target.queryParam(QUERY_PARAMETER_SORT_ORDER,
133          sortOrder.getName());
134    }
135    if(startIndex != null && count != null)
136    {
137      target = target.queryParam(QUERY_PARAMETER_PAGE_START_INDEX, startIndex);
138      target = target.queryParam(QUERY_PARAMETER_PAGE_SIZE, count);
139    }
140    return target;
141  }
142
143  /**
144   * Invoke the SCIM retrieve request using GET.
145   *
146   * @param <T> The type of objects to return.
147   * @param cls The Java class object used to determine the type to return.
148   * @return The ListResponse containing the search results.
149   * @throws ScimException If an error occurred.
150   */
151  public <T> ListResponse<T> invoke(final Class<T> cls)
152      throws ScimException
153  {
154    ListResponseBuilder<T> listResponseBuilder = new ListResponseBuilder<T>();
155    invoke(false, listResponseBuilder, cls);
156    return listResponseBuilder.build();
157  }
158
159  /**
160   * Invoke the SCIM retrieve request using GET.
161   *
162   * @param <T> The type of objects to return.
163   * @param resultHandler The search result handler that should be used to
164   *                      process the resources.
165   * @param cls The Java class object used to determine the type to return.
166   * @throws ScimException If an error occurred.
167   */
168  public <T> void invoke(
169      final SearchResultHandler<T> resultHandler,
170      final Class<T> cls) throws ScimException
171  {
172    invoke(false, resultHandler, cls);
173  }
174
175  /**
176   * Invoke the SCIM retrieve request using POST.
177   *
178   * @param <T> The type of objects to return.
179   * @param cls The Java class object used to determine the type to return.
180   * @return The ListResponse containing the search results.
181   * @throws ScimException If an error occurred.
182   */
183  public <T extends ScimResource> ListResponse<T> invokePost(final Class<T> cls)
184      throws ScimException
185  {
186    ListResponseBuilder<T> listResponseBuilder = new ListResponseBuilder<T>();
187    invoke(true, listResponseBuilder, cls);
188    return listResponseBuilder.build();
189  }
190
191  /**
192   * Invoke the SCIM retrieve request using POST.
193   *
194   * @param <T> The type of objects to return.
195   * @param resultHandler The search result handler that should be used to
196   *                      process the resources.
197   * @param cls The Java class object used to determine the type to return.
198   * @throws ScimException If an error occurred.
199   */
200  public <T> void invokePost(
201      final SearchResultHandler<T> resultHandler, final Class<T> cls)
202      throws ScimException
203  {
204    invoke(true, resultHandler, cls);
205  }
206
207  /**
208   * Invoke the SCIM retrieve request.
209   *
210   * @param post {@code true} to send the request using POST or {@code false}
211   *             to send the request using GET.
212   * @param <T> The type of objects to return.
213   * @param resultHandler The search result handler that should be used to
214   *                      process the resources.
215   * @param cls The Java class object used to determine the type to return.
216   * @throws javax.ws.rs.ProcessingException If a JAX-RS runtime exception occurred.
217   * @throws ScimException If the SCIM service provider responded with an error.
218   */
219  private <T> void invoke(
220      final boolean post, final SearchResultHandler<T> resultHandler,
221      final Class<T> cls)
222      throws ScimException
223  {
224    Response response;
225    if(post)
226    {
227      Set<String> attributeSet = null;
228      Set<String> excludedAttributeSet = null;
229      if(attributes != null && attributes.size() > 0)
230      {
231        if(!excluded)
232        {
233          attributeSet = attributes;
234        }
235        else
236        {
237          excludedAttributeSet = attributes;
238        }
239      }
240
241      SearchRequest searchRequest = new SearchRequest(attributeSet,
242          excludedAttributeSet, filter, sortBy, sortOrder, startIndex, count);
243
244      Invocation.Builder builder = target().
245          path(ApiConstants.SEARCH_WITH_POST_PATH_EXTENSION).
246          request(ScimService.MEDIA_TYPE_SCIM_TYPE,
247                  MediaType.APPLICATION_JSON_TYPE);
248      for (Map.Entry<String, List<Object>> header : headers.entrySet())
249      {
250        builder = builder.header(header.getKey(),
251                                 StaticUtils.listToString(header.getValue(),
252                                                          ", "));
253      }
254      response = builder.post(Entity.entity(searchRequest,
255                                            getContentType()));
256    }
257    else
258    {
259      response = buildRequest().get();
260    }
261
262    try
263    {
264      if (response.getStatusInfo().getFamily() ==
265          Response.Status.Family.SUCCESSFUL)
266      {
267        InputStream inputStream = response.readEntity(InputStream.class);
268        try
269        {
270          JsonParser parser = JsonUtils.getObjectReader().
271              getFactory().createParser(inputStream);
272          try
273          {
274            parser.nextToken();
275            boolean stop = false;
276            while (!stop && parser.nextToken() != JsonToken.END_OBJECT)
277            {
278              String field = parser.getCurrentName();
279              parser.nextToken();
280              if (field.equals("schemas"))
281              {
282                parser.skipChildren();
283              } else if (field.equals("totalResults"))
284              {
285                resultHandler.totalResults(parser.getIntValue());
286              } else if (field.equals("startIndex"))
287              {
288                resultHandler.startIndex(parser.getIntValue());
289              } else if (field.equals("itemsPerPage"))
290              {
291                resultHandler.itemsPerPage(parser.getIntValue());
292              } else if (field.equals("Resources"))
293              {
294                while (parser.nextToken() != JsonToken.END_ARRAY)
295                {
296                  if (!resultHandler.resource(parser.readValueAs(cls)))
297                  {
298                    stop = true;
299                    break;
300                  }
301                }
302              } else if (SchemaUtils.isUrn(field))
303              {
304                resultHandler.extension(
305                    field, parser.<ObjectNode>readValueAsTree());
306              } else
307              {
308                // Just skip this field
309                parser.nextToken();
310              }
311            }
312          }
313          finally
314          {
315            if(inputStream != null)
316            {
317              inputStream.close();
318            }
319            parser.close();
320          }
321        }
322        catch (IOException e)
323        {
324          throw new ResponseProcessingException(response, e);
325        }
326      }
327      else
328      {
329        throw toScimException(response);
330      }
331    }
332    finally
333    {
334      response.close();
335    }
336  }
337}