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.server.utils;
019
020import com.unboundid.scim2.common.GenericScimResource;
021import com.unboundid.scim2.common.Path;
022import com.unboundid.scim2.common.ScimResource;
023import com.unboundid.scim2.common.annotations.NotNull;
024import com.unboundid.scim2.common.annotations.Nullable;
025import com.unboundid.scim2.common.exceptions.BadRequestException;
026import com.unboundid.scim2.common.exceptions.ScimException;
027import com.unboundid.scim2.common.filters.Filter;
028import com.unboundid.scim2.common.messages.SortOrder;
029import com.unboundid.scim2.server.ListResponseStreamingOutput;
030import com.unboundid.scim2.server.ListResponseWriter;
031
032import jakarta.ws.rs.core.MultivaluedMap;
033import jakarta.ws.rs.core.UriInfo;
034import java.io.IOException;
035import java.util.Collection;
036import java.util.Collections;
037import java.util.LinkedList;
038import java.util.List;
039
040import static com.unboundid.scim2.common.utils.ApiConstants.*;
041
042/**
043 * A utility ListResponseStreamingOutput that will filter, sort, and paginate
044 * the search results for simple search implementations that always returns the
045 * entire result set.
046 */
047public class SimpleSearchResults<T extends ScimResource>
048    extends ListResponseStreamingOutput<T>
049{
050  @NotNull
051  private final List<ScimResource> resources;
052
053  @NotNull
054  private final Filter filter;
055
056  @Nullable
057  private final Integer startIndex;
058
059  @Nullable
060  private final Integer count;
061
062  @NotNull
063  private final SchemaAwareFilterEvaluator filterEvaluator;
064
065  @Nullable
066  private final ResourceComparator<ScimResource> resourceComparator;
067
068  @NotNull
069  private final ResourcePreparer<ScimResource> responsePreparer;
070
071  /**
072   * Create a new SimpleSearchResults for results from a search operation.
073   *
074   * @param resourceType The resource type definition of result resources.
075   * @param uriInfo The UriInfo from the search operation.
076   * @throws BadRequestException if the filter or paths in the search operation
077   * is invalid.
078   */
079  public SimpleSearchResults(@NotNull final ResourceTypeDefinition resourceType,
080                             @NotNull final UriInfo uriInfo)
081      throws BadRequestException
082  {
083    this.filterEvaluator = new SchemaAwareFilterEvaluator(resourceType);
084    this.responsePreparer =
085        new ResourcePreparer<ScimResource>(resourceType, uriInfo);
086    this.resources = new LinkedList<ScimResource>();
087
088    MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
089    String filterString = queryParams.getFirst(QUERY_PARAMETER_FILTER);
090    String startIndexString = queryParams.getFirst(
091        QUERY_PARAMETER_PAGE_START_INDEX);
092    String countString = queryParams.getFirst(QUERY_PARAMETER_PAGE_SIZE);
093    String sortByString = queryParams.getFirst(QUERY_PARAMETER_SORT_BY);
094    String  sortOrderString = queryParams.getFirst(QUERY_PARAMETER_SORT_ORDER);
095
096    if(filterString != null)
097    {
098      this.filter = Filter.fromString(filterString);
099    }
100    else
101    {
102      this.filter = null;
103    }
104
105    if(startIndexString != null)
106    {
107      int i = Integer.valueOf(startIndexString);
108      // 3.4.2.4: A value less than 1 SHALL be interpreted as 1.
109      startIndex = i < 1 ? 1 : i;
110    }
111    else
112    {
113      startIndex = null;
114    }
115
116    if(countString != null)
117    {
118      int i = Integer.valueOf(countString);
119      // 3.4.2.4: A negative value SHALL be interpreted as 0.
120      count = i < 0 ? 0 : i;
121    }
122    else
123    {
124      count = null;
125    }
126
127    Path sortBy;
128    try
129    {
130      sortBy = sortByString == null ? null : Path.fromString(sortByString);
131    }
132    catch (BadRequestException e)
133    {
134      throw BadRequestException.invalidValue("'" + sortByString +
135          "' is not a valid value for the sortBy parameter: " +
136          e.getMessage());
137    }
138    SortOrder sortOrder = sortOrderString == null ?
139        SortOrder.ASCENDING : SortOrder.fromName(sortOrderString);
140    if(sortBy != null)
141    {
142      this.resourceComparator = new ResourceComparator<ScimResource>(
143          sortBy, sortOrder, resourceType);
144    }
145    else
146    {
147      this.resourceComparator = null;
148    }
149  }
150
151  /**
152   * Add a resource to include in the search results.
153   *
154   * @param resource The resource to add.
155   * @return this object.
156   * @throws ScimException If an error occurs during filtering or setting the
157   * meta attributes.
158   */
159  @NotNull
160  public SimpleSearchResults add(@NotNull final T resource) throws ScimException
161  {
162    // Convert to GenericScimResource
163    GenericScimResource genericResource;
164    if(resource instanceof GenericScimResource)
165    {
166      // Make a copy
167      genericResource = new GenericScimResource(
168          ((GenericScimResource) resource).getObjectNode().deepCopy());
169    }
170    else
171    {
172      genericResource = resource.asGenericScimResource();
173    }
174
175    // Set meta attributes so they can be used in the following filter eval
176    responsePreparer.setResourceTypeAndLocation(genericResource);
177
178    if(filter == null || filter.visit(filterEvaluator,
179        genericResource.getObjectNode()))
180    {
181      resources.add(genericResource);
182    }
183
184    return this;
185  }
186
187  /**
188   * Add resources to include in the search results.
189   *
190   * @param resources The resources to add.
191   * @return this object.
192   * @throws ScimException If an error occurs during filtering or setting the
193   * meta attributes.
194   */
195  @NotNull
196  public SimpleSearchResults addAll(@NotNull final Collection<T> resources)
197      throws ScimException
198  {
199    for(T resource : resources)
200    {
201      add(resource);
202    }
203    return this;
204  }
205
206  /**
207   * {@inheritDoc}
208   */
209  @Override
210  @SuppressWarnings("unchecked")
211  public void write(@NotNull final ListResponseWriter<T> os)
212      throws IOException
213  {
214    if(resourceComparator != null)
215    {
216      Collections.sort(resources, resourceComparator);
217    }
218    List<ScimResource> resultsToReturn = resources;
219    if(startIndex != null)
220    {
221      if(startIndex > resources.size())
222      {
223        resultsToReturn = Collections.emptyList();
224      }
225      else
226      {
227        resultsToReturn = resources.subList(startIndex - 1, resources.size());
228      }
229    }
230    if(count != null && !resultsToReturn.isEmpty())
231    {
232      resultsToReturn = resultsToReturn.subList(
233          0, Math.min(count, resultsToReturn.size()));
234    }
235    os.totalResults(resources.size());
236    if(startIndex != null || count != null)
237    {
238      os.startIndex(startIndex == null ? 1 : startIndex);
239      os.itemsPerPage(resultsToReturn.size());
240    }
241    for(ScimResource resource : resultsToReturn)
242    {
243      os.resource((T) responsePreparer.trimRetrievedResource(resource));
244    }
245  }
246}