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