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.unboundid.scim2.client.ScimServiceException; 021import com.unboundid.scim2.common.ScimResource; 022import com.unboundid.scim2.common.annotations.NotNull; 023import com.unboundid.scim2.common.annotations.Nullable; 024import com.unboundid.scim2.common.exceptions.ScimException; 025import com.unboundid.scim2.common.messages.ErrorResponse; 026import com.unboundid.scim2.common.utils.StaticUtils; 027 028import jakarta.ws.rs.ProcessingException; 029import jakarta.ws.rs.client.Invocation; 030import jakarta.ws.rs.client.WebTarget; 031import jakarta.ws.rs.core.MediaType; 032import jakarta.ws.rs.core.MultivaluedHashMap; 033import jakarta.ws.rs.core.MultivaluedMap; 034import jakarta.ws.rs.core.Response; 035import java.util.ArrayList; 036import java.util.List; 037import java.util.Map; 038 039import static com.unboundid.scim2.common.utils.ApiConstants.MEDIA_TYPE_SCIM; 040 041/** 042 * Abstract SCIM request builder. 043 */ 044public class RequestBuilder<T extends RequestBuilder> 045{ 046 /** 047 * The web target to send the request. 048 */ 049 @NotNull 050 private WebTarget target; 051 052 /** 053 * Arbitrary request headers. 054 */ 055 @NotNull 056 protected final MultivaluedMap<String, Object> headers = 057 new MultivaluedHashMap<String, Object>(); 058 059 /** 060 * Arbitrary query parameters. 061 */ 062 @NotNull 063 protected final MultivaluedMap<String, Object> queryParams = 064 new MultivaluedHashMap<String, Object>(); 065 066 @Nullable 067 private String contentType = MEDIA_TYPE_SCIM; 068 069 @NotNull 070 private List<String> accept = new ArrayList<String>(); 071 072 /** 073 * Create a new SCIM request builder. 074 * 075 * @param target The WebTarget to send the request. 076 */ 077 RequestBuilder(@NotNull final WebTarget target) 078 { 079 this.target = target; 080 accept(MEDIA_TYPE_SCIM, MediaType.APPLICATION_JSON); 081 } 082 083 /** 084 * Add an arbitrary HTTP header to the request. 085 * 086 * @param name The header name. 087 * @param value The header value(s). 088 * @return This builder. 089 */ 090 @NotNull 091 @SuppressWarnings("unchecked") 092 public T header(@NotNull final String name, @NotNull final Object... value) 093 { 094 headers.addAll(name, value); 095 return (T) this; 096 } 097 098 /** 099 * Sets the media type for any content sent to the server. The default 100 * value is ApiConstants.MEDIA_TYPE_SCIM ("application/scim+json"). 101 * 102 * @param contentType a string describing the media type of content 103 * sent to the server. 104 * @return This builder. 105 */ 106 @NotNull 107 public T contentType(@Nullable final String contentType) 108 { 109 this.contentType = contentType; 110 return (T) this; 111 } 112 113 /** 114 * Sets the media type(s) that are acceptable as a return from the server. 115 * The default accepted media types are 116 * ApiConstants.MEDIA_TYPE_SCIM ("application/scim+json") and 117 * MediaType.APPLICATION_JSON ("application/json") 118 * 119 * @param acceptStrings a string (or strings) describing the media type that 120 * will be accepted from the server. This parameter may 121 * not be null. 122 * @return This builder. 123 */ 124 @NotNull 125 public T accept(@NotNull final String... acceptStrings) 126 { 127 this.accept.clear(); 128 if((acceptStrings == null) || (acceptStrings.length == 0)) 129 { 130 throw new IllegalArgumentException( 131 "Accepted media types must not be null or empty"); 132 } 133 134 for(String acceptString : acceptStrings) 135 { 136 accept.add(acceptString); 137 } 138 139 return (T) this; 140 } 141 142 /** 143 * Add an arbitrary query parameter to the request. 144 * 145 * @param name The query parameter name. 146 * @param value The query parameter value(s). 147 * @return This builder. 148 */ 149 @NotNull 150 @SuppressWarnings("unchecked") 151 public T queryParam(@NotNull final String name, 152 @NotNull final Object... value) 153 { 154 queryParams.addAll(name, value); 155 return (T) this; 156 } 157 158 /** 159 * Retrieve the meta.version attribute of the resource. 160 * 161 * @param resource The resource whose version to retrieve. 162 * @return The resource version. 163 * @throws IllegalArgumentException if the resource does not contain a the 164 * meta.version attribute. 165 */ 166 @NotNull 167 static String getResourceVersion(@NotNull final ScimResource resource) 168 throws IllegalArgumentException 169 { 170 if(resource == null || resource.getMeta() == null || 171 resource.getMeta().getVersion() == null) 172 { 173 throw new IllegalArgumentException( 174 "Resource version must be specified by meta.version"); 175 } 176 return resource.getMeta().getVersion(); 177 } 178 179 /** 180 * Convert a JAX-RS response to a ScimException. 181 * 182 * @param response The JAX-RS response. 183 * @return the converted ScimException. 184 */ 185 @NotNull 186 static ScimException toScimException(@NotNull final Response response) 187 { 188 try 189 { 190 ErrorResponse errorResponse = response.readEntity(ErrorResponse.class); 191 // If are able to read an error response, use it to build the exception. 192 // If not, use the http status code to determine the exception. 193 ScimException exception = (errorResponse == null) ? 194 ScimException.createException(response.getStatus(), null) : 195 ScimException.createException(errorResponse, null); 196 response.close(); 197 198 return exception; 199 } 200 catch(ProcessingException ex) 201 { 202 // The exception message likely contains unwanted details about why the 203 // server failed to process the response, instead of the actual SCIM 204 // issue. Replace it with a general reason phrase for the status code. 205 String genericDetails = response.getStatusInfo().getReasonPhrase(); 206 207 return new ScimServiceException( 208 response.getStatus(), genericDetails, ex); 209 } 210 } 211 212 /** 213 * Returns the unbuilt WebTarget for the request. In most cases, 214 * {@link #buildTarget()} should be used instead. 215 * 216 * @return The WebTarget for the request. 217 */ 218 @NotNull 219 protected WebTarget target() 220 { 221 return target; 222 } 223 224 /** 225 * Build the WebTarget for the request. 226 * 227 * @return The WebTarget for the request. 228 */ 229 @NotNull 230 WebTarget buildTarget() 231 { 232 for(Map.Entry<String, List<Object>> queryParam : queryParams.entrySet()) 233 { 234 target = target.queryParam(queryParam.getKey(), 235 queryParam.getValue().toArray()); 236 } 237 return target; 238 } 239 240 /** 241 * Gets the media type for any content sent to the server. 242 * 243 * @return the media type for any content sent to the server. 244 */ 245 @Nullable 246 protected String getContentType() 247 { 248 return contentType; 249 } 250 251 /** 252 * Gets the media type(s) that are acceptable as a return from the server. 253 * 254 * @return the media type(s) that are acceptable as a return from the server. 255 */ 256 @NotNull 257 protected List<String> getAccept() 258 { 259 return accept; 260 } 261 /** 262 * Build the Invocation.Builder for the request. 263 * 264 * @return The Invocation.Builder for the request. 265 */ 266 @NotNull 267 Invocation.Builder buildRequest() 268 { 269 Invocation.Builder builder = 270 buildTarget().request(accept.toArray(new String[accept.size()])); 271 for(Map.Entry<String, List<Object>> header : headers.entrySet()) 272 { 273 builder = builder.header(header.getKey(), 274 StaticUtils.listToString(header.getValue(), 275 ", ")); 276 } 277 return builder; 278 } 279}