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}