001/* 002 * Copyright 2015-2019 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.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}