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