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.databind.JsonNode;
021import com.unboundid.scim2.common.Path;
022import com.unboundid.scim2.common.ScimResource;
023import com.unboundid.scim2.common.annotations.NotNull;
024import com.unboundid.scim2.common.annotations.Nullable;
025import com.unboundid.scim2.common.exceptions.ScimException;
026import com.unboundid.scim2.common.messages.PatchOperation;
027import com.unboundid.scim2.common.messages.PatchRequest;
028import com.unboundid.scim2.common.utils.JsonUtils;
029
030import jakarta.ws.rs.ProcessingException;
031import jakarta.ws.rs.client.Entity;
032import jakarta.ws.rs.client.Invocation;
033import jakarta.ws.rs.client.WebTarget;
034import jakarta.ws.rs.core.HttpHeaders;
035import jakarta.ws.rs.core.Response;
036
037import java.util.Collection;
038import java.util.LinkedList;
039import java.util.List;
040
041/**
042 * A builder for SCIM modify requests.
043 */
044public abstract class ModifyRequestBuilder<T extends ModifyRequestBuilder<T>>
045    extends ResourceReturningRequestBuilder<T>
046{
047  /**
048   * The list of patch operations to include in the request.
049   */
050  @NotNull
051  protected final List<PatchOperation> operations;
052
053  /**
054   * The version to match.
055   */
056  @Nullable
057  protected String version;
058
059  /**
060   * Create a new ModifyRequestBuilder.
061   *
062   * @param target The WebTarget to PATCH.
063   */
064  private ModifyRequestBuilder(@NotNull final WebTarget target)
065  {
066    super(target);
067    this.operations = new LinkedList<PatchOperation>();
068  }
069
070  /**
071   * {@inheritDoc}
072   */
073  @Override
074  @NotNull
075  Invocation.Builder buildRequest()
076  {
077    Invocation.Builder request = super.buildRequest();
078    if(version != null)
079    {
080      request.header(HttpHeaders.IF_MATCH, version);
081    }
082    return request;
083  }
084
085  /**
086   * A builder for SCIM modify requests for where the returned resource POJO
087   * type will be the same as the original.
088   */
089  public static final class Generic<T extends ScimResource>
090      extends ModifyRequestBuilder<Generic<T>>
091  {
092    @NotNull
093    private final T resource;
094
095    /**
096     * Create a new generic modify request builder.
097     *
098     * @param target The WebTarget to PATCH.
099     * @param resource The SCIM resource to retrieve.
100     */
101    public Generic(@NotNull final WebTarget target, @NotNull final T resource)
102    {
103      super(target);
104      this.resource = resource;
105    }
106
107
108    /**
109     * Modify the resource only if the resource has not been modified from the
110     * resource provided.
111     *
112     * @return This builder.
113     */
114    @NotNull
115    public Generic<T> ifMatch()
116    {
117      this.version = getResourceVersion(resource);
118      return this;
119    }
120
121    /**
122     * Invoke the SCIM modify request.
123     *
124     * @return The successfully modified SCIM resource.
125     * @throws ProcessingException If a JAX-RS runtime exception occurred.
126     * @throws ScimException If the SCIM service provider responded with an error.
127     */
128    @NotNull
129    @SuppressWarnings("unchecked")
130    public T invoke() throws ScimException
131    {
132      return (T) invoke(resource.getClass());
133    }
134
135    /**
136     * Invoke the SCIM modify request.
137     *
138     * @param <C> The type of object to return.
139     * @param cls The Java class object used to determine the type to return.
140     * @return The successfully modified SCIM resource.
141     * @throws ProcessingException If a JAX-RS runtime exception occurred.
142     * @throws ScimException If the SCIM service provider responded with an error.
143     */
144    @NotNull
145    public <C> C invoke(@NotNull final Class<C> cls) throws ScimException
146    {
147      PatchRequest patchRequest = new PatchRequest(operations);
148      Response response = buildRequest().method("PATCH",
149          Entity.entity(patchRequest, getContentType()));
150      try
151      {
152        if (response.getStatusInfo().getFamily() ==
153            Response.Status.Family.SUCCESSFUL)
154        {
155          return response.readEntity(cls);
156        } else
157        {
158          throw toScimException(response);
159        }
160      }
161      finally
162      {
163        response.close();
164      }
165    }
166  }
167
168
169  /**
170   * A builder for SCIM modify requests for where the returned resource POJO
171   * type will be provided.
172   */
173  public static final class Typed extends ModifyRequestBuilder<Typed>
174  {
175    /**
176     * Create a new generic modify request builder.
177     *
178     * @param target The WebTarget to PATCH.
179     */
180    public Typed(@NotNull final WebTarget target)
181    {
182      super(target);
183    }
184
185    /**
186     * Modify the resource only if the resource has not been modified since the
187     * provided version.
188     *
189     * @param version The version of the resource to compare.
190     * @return This builder.
191     */
192    @NotNull
193    public Typed ifMatch(@Nullable final String version)
194    {
195      this.version = version;
196      return this;
197    }
198
199    /**
200     * Invoke the SCIM modify request.
201     *
202     * @param <T> The type of object to return.
203     * @param cls The Java class object used to determine the type to return.
204     * @return The successfully modified SCIM resource.
205     * @throws ProcessingException If a JAX-RS runtime exception occurred.
206     * @throws ScimException If the SCIM service provider responded with an error.
207     */
208    @NotNull
209    public <T> T invoke(@NotNull final Class<T> cls) throws ScimException
210    {
211      PatchRequest patchRequest = new PatchRequest(operations);
212      Response response = buildRequest().method("PATCH",
213          Entity.entity(patchRequest, getContentType()));
214      try
215      {
216        if(response.getStatusInfo().getFamily() ==
217            Response.Status.Family.SUCCESSFUL)
218        {
219          return response.readEntity(cls);
220        }
221        else
222        {
223          throw toScimException(response);
224        }
225      }
226      finally
227      {
228        response.close();
229      }
230    }
231  }
232
233  /**
234   * Set value of the attribute specified by the path, replacing any existing
235   * value(s).
236   *
237   * @param path The path to the attribute whose value to set.
238   * @param object The value to set.
239   *
240   * @return This patch operation request.
241   * @throws ScimException If the path is invalid.
242   */
243  @NotNull
244  public T replaceValue(@NotNull final String path,
245                        @Nullable final Object object)
246      throws ScimException
247  {
248    return replaceValue(Path.fromString(path), object);
249  }
250
251  /**
252   * Set value of the attribute specified by the path, replacing any existing
253   * value(s).
254   *
255   * @param path The path to the attribute whose value to set.
256   * @param object The value to set.
257   *
258   * @return This patch operation request.
259   */
260  @NotNull
261  public T replaceValue(@NotNull final Path path, @Nullable final Object object)
262  {
263    JsonNode newObjectNode = JsonUtils.valueToNode(object);
264    return addOperation(PatchOperation.replace(path, newObjectNode));
265  }
266
267  /**
268   * Set values of the attribute specified by the path, replacing any existing
269   * values.
270   *
271   * @param path The path to the attribute whose value to set.
272   * @param objects The value(s) to set.
273   *
274   * @return This patch operation request.
275   * @throws ScimException If the path is invalid.
276   */
277  @NotNull
278  public T replaceValues(@NotNull final String path,
279                         @Nullable final Collection<Object> objects)
280      throws ScimException
281  {
282    return replaceValues(Path.fromString(path), objects);
283  }
284
285  /**
286   * Set values of the attribute specified by the path, replacing any existing
287   * values.
288   *
289   * @param path The path to the attribute whose value to set.
290   * @param objects The value(s) to set.
291   *
292   * @return This patch operation request.
293   */
294  @NotNull
295  public T replaceValues(@NotNull final Path path,
296                         @Nullable final Collection<Object> objects)
297  {
298    JsonNode newObjectNode = JsonUtils.valueToNode(objects);
299    return addOperation(PatchOperation.replace(path, newObjectNode));
300  }
301
302  /**
303   * Set values of the attribute specified by the path, replacing any existing
304   * values.
305   *
306   * @param path The path to the attribute whose value to set.
307   * @param objects The value(s) to set.
308   *
309   * @return This patch operation request.
310   * @throws ScimException If the path is invalid.
311   */
312  @NotNull
313  public T replaceValues(@NotNull final String path,
314                         @NotNull final Object... objects)
315      throws ScimException
316  {
317    return replaceValues(Path.fromString(path), objects);
318
319  }
320
321  /**
322   * Set values of the attribute specified by the path, replacing any existing
323   * values.
324   *
325   * @param path The path to the attribute whose value to set.
326   * @param objects The value(s) to set.
327   *
328   * @return This patch operation request.
329   */
330  @NotNull
331  public T replaceValues(@NotNull final Path path,
332                         @NotNull final Object... objects)
333  {
334    JsonNode newObjectNode = JsonUtils.valueToNode(objects);
335    return addOperation(PatchOperation.replace(path, newObjectNode));
336  }
337
338  /**
339   * Add values to the multi-valued attribute specified by the path.
340   *
341   * @param path The path to the multi-valued attribute.
342   * @param objects The values to add.
343   *
344   * @return This patch operation request.
345   * @throws ScimException If the path is invalid.
346   */
347  @NotNull
348  public T addValues(@NotNull final String path,
349                     @NotNull final Collection<?> objects)
350      throws ScimException
351  {
352    return addValues(Path.fromString(path), objects);
353  }
354
355  /**
356   * Add values to the multi-valued attribute specified by the path.
357   *
358   * @param path The path to the multi-valued attribute.
359   * @param objects The values to add.
360   *
361   * @return This patch operation request.
362   */
363  @NotNull
364  public T addValues(@NotNull final Path path,
365                     @NotNull final Collection<?> objects)
366  {
367    JsonNode newObjectNode = JsonUtils.valueToNode(objects);
368    return addOperation(PatchOperation.add(path, newObjectNode));
369  }
370
371  /**
372   * Add values to the multi-valued attribute specified by the path.
373   *
374   * @param path The path to the multi-valued attribute.
375   * @param objects The values to add.
376   *
377   * @return This patch operation request.
378   * @throws ScimException If the path is invalid.
379   */
380  @NotNull
381  public T addValues(@NotNull final String path,
382                     @NotNull final Object... objects)
383      throws ScimException
384  {
385    return addValues(Path.fromString(path), objects);
386
387
388  }
389
390  /**
391   * Add values to the multi-valued attribute specified by the path.
392   *
393   * @param path The path to the multi-valued attribute.
394   * @param objects The values to add.
395   *
396   * @return This patch operation request.
397   */
398  @NotNull
399  public T addValues(@NotNull final Path path,
400                     @NotNull final Object... objects)
401  {
402    JsonNode newObjectNode = JsonUtils.valueToNode(objects);
403    return addOperation(PatchOperation.add(path, newObjectNode));
404  }
405
406  /**
407   * Remove all values of the attribute specified by the path.
408   *
409   * @param path The path to the attribute whose value to remove.
410   *
411   * @return This patch operation request.
412   * @throws ScimException If the path is invalid.
413   */
414  @NotNull
415  public T removeValues(@NotNull final String path)
416      throws ScimException
417  {
418    return removeValues(Path.fromString(path));
419
420  }
421
422  /**
423   * Remove all values of the attribute specified by the path.
424   *
425   * @param path The path to the attribute whose value to remove.
426   *
427   * @return This patch operation request.
428   * @throws ScimException If the path is invalid.
429   */
430  @NotNull
431  public T removeValues(@NotNull final Path path)
432      throws ScimException
433  {
434    return addOperation(PatchOperation.remove(path));
435  }
436
437  /**
438   * Add a new patch operation this this patch request.
439   *
440   * @param op The patch operation to add.
441   *
442   * @return This patch operation request.
443   */
444  @NotNull
445  @SuppressWarnings("unchecked")
446  public T addOperation(@NotNull final PatchOperation op)
447  {
448    operations.add(op);
449    return (T) this;
450  }
451}