001/* 002 * Copyright 2015-2023 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.exceptions.ScimException; 027import com.unboundid.scim2.common.messages.ListResponse; 028import com.unboundid.scim2.common.messages.SearchRequest; 029import com.unboundid.scim2.common.messages.SortOrder; 030import com.unboundid.scim2.common.utils.ApiConstants; 031import com.unboundid.scim2.common.utils.JsonUtils; 032import com.unboundid.scim2.common.utils.SchemaUtils; 033import com.unboundid.scim2.common.utils.StaticUtils; 034 035import javax.ws.rs.client.Entity; 036import javax.ws.rs.client.Invocation; 037import javax.ws.rs.client.ResponseProcessingException; 038import javax.ws.rs.client.WebTarget; 039import javax.ws.rs.core.MediaType; 040import javax.ws.rs.core.Response; 041import java.io.IOException; 042import java.io.InputStream; 043import java.util.List; 044import java.util.Map; 045import java.util.Set; 046 047import static com.unboundid.scim2.common.utils.ApiConstants.QUERY_PARAMETER_FILTER; 048import static com.unboundid.scim2.common.utils.ApiConstants.QUERY_PARAMETER_PAGE_SIZE; 049import static com.unboundid.scim2.common.utils.ApiConstants.QUERY_PARAMETER_PAGE_START_INDEX; 050import static com.unboundid.scim2.common.utils.ApiConstants.QUERY_PARAMETER_SORT_BY; 051import static com.unboundid.scim2.common.utils.ApiConstants.QUERY_PARAMETER_SORT_ORDER; 052 053/** 054 * A builder for SCIM search requests. 055 */ 056public final class SearchRequestBuilder 057 extends ResourceReturningRequestBuilder<SearchRequestBuilder> 058{ 059 private String filter; 060 private String sortBy; 061 private SortOrder sortOrder; 062 private Integer startIndex; 063 private Integer count; 064 065 /** 066 * Create a new search request builder. 067 * 068 * @param target The WebTarget to search. 069 */ 070 public SearchRequestBuilder(final WebTarget target) 071 { 072 super(target); 073 } 074 075 /** 076 * Request filtering of resources. 077 * 078 * @param filter the filter string used to request a subset of resources. 079 * @return This builder. 080 */ 081 public SearchRequestBuilder filter(final String filter) 082 { 083 this.filter = filter; 084 return this; 085 } 086 087 /** 088 * Request sorting of resources. 089 * 090 * @param sortBy the string indicating the attribute whose value shall be used 091 * to order the returned responses. 092 * @param sortOrder the order in which the sortBy parameter is applied. 093 * @return This builder. 094 */ 095 public SearchRequestBuilder sort(final String sortBy, 096 final SortOrder sortOrder) 097 { 098 this.sortBy = sortBy; 099 this.sortOrder = sortOrder; 100 return this; 101 } 102 103 /** 104 * Request pagination of resources. 105 * 106 * @param startIndex the 1-based index of the first query result. 107 * @param count the desired maximum number of query results per page. 108 * @return This builder. 109 */ 110 public SearchRequestBuilder page(final int startIndex, 111 final int count) 112 { 113 this.startIndex = startIndex; 114 this.count = count; 115 return this; 116 } 117 118 /** 119 * {@inheritDoc} 120 */ 121 @Override 122 WebTarget buildTarget() 123 { 124 WebTarget target = super.buildTarget(); 125 if(filter != null) 126 { 127 target = target.queryParam(QUERY_PARAMETER_FILTER, filter); 128 } 129 if(sortBy != null && sortOrder != null) 130 { 131 target = target.queryParam(QUERY_PARAMETER_SORT_BY, sortBy); 132 target = target.queryParam(QUERY_PARAMETER_SORT_ORDER, 133 sortOrder.getName()); 134 } 135 if(startIndex != null && count != null) 136 { 137 target = target.queryParam(QUERY_PARAMETER_PAGE_START_INDEX, startIndex); 138 target = target.queryParam(QUERY_PARAMETER_PAGE_SIZE, count); 139 } 140 return target; 141 } 142 143 /** 144 * Invoke the SCIM retrieve request using GET. 145 * 146 * @param <T> The type of objects to return. 147 * @param cls The Java class object used to determine the type to return. 148 * @return The ListResponse containing the search results. 149 * @throws ScimException If an error occurred. 150 */ 151 public <T> ListResponse<T> invoke(final Class<T> cls) 152 throws ScimException 153 { 154 ListResponseBuilder<T> listResponseBuilder = new ListResponseBuilder<T>(); 155 invoke(false, listResponseBuilder, cls); 156 return listResponseBuilder.build(); 157 } 158 159 /** 160 * Invoke the SCIM retrieve request using GET. 161 * 162 * @param <T> The type of objects to return. 163 * @param resultHandler The search result handler that should be used to 164 * process the resources. 165 * @param cls The Java class object used to determine the type to return. 166 * @throws ScimException If an error occurred. 167 */ 168 public <T> void invoke( 169 final SearchResultHandler<T> resultHandler, 170 final Class<T> cls) throws ScimException 171 { 172 invoke(false, resultHandler, cls); 173 } 174 175 /** 176 * Invoke the SCIM retrieve request using POST. 177 * 178 * @param <T> The type of objects to return. 179 * @param cls The Java class object used to determine the type to return. 180 * @return The ListResponse containing the search results. 181 * @throws ScimException If an error occurred. 182 */ 183 public <T extends ScimResource> ListResponse<T> invokePost(final Class<T> cls) 184 throws ScimException 185 { 186 ListResponseBuilder<T> listResponseBuilder = new ListResponseBuilder<T>(); 187 invoke(true, listResponseBuilder, cls); 188 return listResponseBuilder.build(); 189 } 190 191 /** 192 * Invoke the SCIM retrieve request using POST. 193 * 194 * @param <T> The type of objects to return. 195 * @param resultHandler The search result handler that should be used to 196 * process the resources. 197 * @param cls The Java class object used to determine the type to return. 198 * @throws ScimException If an error occurred. 199 */ 200 public <T> void invokePost( 201 final SearchResultHandler<T> resultHandler, final Class<T> cls) 202 throws ScimException 203 { 204 invoke(true, resultHandler, cls); 205 } 206 207 /** 208 * Invoke the SCIM retrieve request. 209 * 210 * @param post {@code true} to send the request using POST or {@code false} 211 * to send the request using GET. 212 * @param <T> The type of objects to return. 213 * @param resultHandler The search result handler that should be used to 214 * process the resources. 215 * @param cls The Java class object used to determine the type to return. 216 * @throws javax.ws.rs.ProcessingException If a JAX-RS runtime exception occurred. 217 * @throws ScimException If the SCIM service provider responded with an error. 218 */ 219 private <T> void invoke( 220 final boolean post, final SearchResultHandler<T> resultHandler, 221 final Class<T> cls) 222 throws ScimException 223 { 224 Response response; 225 if(post) 226 { 227 Set<String> attributeSet = null; 228 Set<String> excludedAttributeSet = null; 229 if(attributes != null && attributes.size() > 0) 230 { 231 if(!excluded) 232 { 233 attributeSet = attributes; 234 } 235 else 236 { 237 excludedAttributeSet = attributes; 238 } 239 } 240 241 SearchRequest searchRequest = new SearchRequest(attributeSet, 242 excludedAttributeSet, filter, sortBy, sortOrder, startIndex, count); 243 244 Invocation.Builder builder = target(). 245 path(ApiConstants.SEARCH_WITH_POST_PATH_EXTENSION). 246 request(ScimService.MEDIA_TYPE_SCIM_TYPE, 247 MediaType.APPLICATION_JSON_TYPE); 248 for (Map.Entry<String, List<Object>> header : headers.entrySet()) 249 { 250 builder = builder.header(header.getKey(), 251 StaticUtils.listToString(header.getValue(), 252 ", ")); 253 } 254 response = builder.post(Entity.entity(searchRequest, 255 getContentType())); 256 } 257 else 258 { 259 response = buildRequest().get(); 260 } 261 262 try 263 { 264 if (response.getStatusInfo().getFamily() == 265 Response.Status.Family.SUCCESSFUL) 266 { 267 InputStream inputStream = response.readEntity(InputStream.class); 268 try 269 { 270 JsonParser parser = JsonUtils.getObjectReader(). 271 getFactory().createParser(inputStream); 272 try 273 { 274 parser.nextToken(); 275 boolean stop = false; 276 while (!stop && parser.nextToken() != JsonToken.END_OBJECT) 277 { 278 String field = parser.getCurrentName(); 279 parser.nextToken(); 280 if (field.equals("schemas")) 281 { 282 parser.skipChildren(); 283 } else if (field.equals("totalResults")) 284 { 285 resultHandler.totalResults(parser.getIntValue()); 286 } else if (field.equals("startIndex")) 287 { 288 resultHandler.startIndex(parser.getIntValue()); 289 } else if (field.equals("itemsPerPage")) 290 { 291 resultHandler.itemsPerPage(parser.getIntValue()); 292 } else if (field.equals("Resources")) 293 { 294 while (parser.nextToken() != JsonToken.END_ARRAY) 295 { 296 if (!resultHandler.resource(parser.readValueAs(cls))) 297 { 298 stop = true; 299 break; 300 } 301 } 302 } else if (SchemaUtils.isUrn(field)) 303 { 304 resultHandler.extension( 305 field, parser.<ObjectNode>readValueAsTree()); 306 } else 307 { 308 // Just skip this field 309 parser.nextToken(); 310 } 311 } 312 } 313 finally 314 { 315 if(inputStream != null) 316 { 317 inputStream.close(); 318 } 319 parser.close(); 320 } 321 } 322 catch (IOException e) 323 { 324 throw new ResponseProcessingException(response, e); 325 } 326 } 327 else 328 { 329 throw toScimException(response); 330 } 331 } 332 finally 333 { 334 response.close(); 335 } 336 } 337}