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}