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}