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