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;
019
020import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
021import com.unboundid.scim2.client.requests.CreateRequestBuilder;
022import com.unboundid.scim2.client.requests.DeleteRequestBuilder;
023import com.unboundid.scim2.client.requests.ModifyRequestBuilder;
024import com.unboundid.scim2.client.requests.ReplaceRequestBuilder;
025import com.unboundid.scim2.client.requests.RetrieveRequestBuilder;
026import com.unboundid.scim2.client.requests.SearchRequestBuilder;
027import com.unboundid.scim2.common.ScimResource;
028import com.unboundid.scim2.common.exceptions.ScimException;
029import com.unboundid.scim2.common.messages.ListResponse;
030import com.unboundid.scim2.common.messages.PatchOperation;
031import com.unboundid.scim2.common.messages.PatchRequest;
032import com.unboundid.scim2.common.types.Meta;
033import com.unboundid.scim2.common.types.ResourceTypeResource;
034import com.unboundid.scim2.common.types.SchemaResource;
035import com.unboundid.scim2.common.types.ServiceProviderConfigResource;
036import com.unboundid.scim2.common.utils.JsonUtils;
037
038import javax.ws.rs.client.WebTarget;
039import javax.ws.rs.core.MediaType;
040import java.net.URI;
041
042import static com.unboundid.scim2.common.utils.ApiConstants.MEDIA_TYPE_SCIM;
043import static com.unboundid.scim2.common.utils.ApiConstants.ME_ENDPOINT;
044import static com.unboundid.scim2.common.utils.ApiConstants.
045    RESOURCE_TYPES_ENDPOINT;
046import static com.unboundid.scim2.common.utils.ApiConstants.SCHEMAS_ENDPOINT;
047import static com.unboundid.scim2.common.utils.ApiConstants.
048    SERVICE_PROVIDER_CONFIG_ENDPOINT;
049
050/**
051 * The main entry point to the client API used to access a SCIM 2 service
052 * provider.
053 */
054public class ScimService implements ScimInterface
055{
056  /**
057   * The authenticated subject alias.
058   */
059  public static final URI ME_URI = URI.create(ME_ENDPOINT);
060
061  /**
062   * The SCIM media type.
063   */
064  public static final MediaType MEDIA_TYPE_SCIM_TYPE =
065      MediaType.valueOf(MEDIA_TYPE_SCIM);
066
067  private final WebTarget baseTarget;
068  private volatile ServiceProviderConfigResource serviceProviderConfig;
069
070  /**
071   * Create a new client instance to the SCIM 2 service provider at the
072   * provided WebTarget. The path of the WebTarget should be the base URI
073   * SCIM 2 service (ie. http://host/scim/v2).
074   *
075   * @param baseTarget The web target for the base URI of the SCIM 2 service
076   *                   provider.
077   */
078  public ScimService(final WebTarget baseTarget)
079  {
080    this.baseTarget = baseTarget.register(
081        new JacksonJaxbJsonProvider(JsonUtils.createObjectMapper(),
082            JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS));
083  }
084
085  /**
086   * Retrieve the service provider configuration.
087   *
088   * @return the service provider configuration.
089   * @throws ScimException if an error occurs.
090   */
091  public ServiceProviderConfigResource getServiceProviderConfig()
092      throws ScimException
093  {
094    if(serviceProviderConfig == null)
095    {
096      serviceProviderConfig = retrieve(
097          baseTarget.path(SERVICE_PROVIDER_CONFIG_ENDPOINT).getUri(),
098          ServiceProviderConfigResource.class);
099    }
100    return serviceProviderConfig;
101  }
102
103  /**
104   * Retrieve the resource types supported by the service provider.
105   *
106   * @return The list of resource types supported by the service provider.
107   * @throws ScimException if an error occurs.
108   */
109  public ListResponse<ResourceTypeResource> getResourceTypes()
110      throws ScimException
111  {
112    return searchRequest(RESOURCE_TYPES_ENDPOINT).
113        invoke(ResourceTypeResource.class);
114  }
115
116  /**
117   * Retrieve a known resource type supported by the service provider.
118   *
119   * @param name The name of the resource type.
120   * @return The resource type with the provided name.
121   * @throws ScimException if an error occurs.
122   */
123  public ResourceTypeResource getResourceType(final String name)
124      throws ScimException
125  {
126    return retrieve(RESOURCE_TYPES_ENDPOINT, name, ResourceTypeResource.class);
127  }
128
129  /**
130   * Retrieve the schemas supported by the service provider.
131   *
132   * @return The list of schemas supported by the service provider.
133   * @throws ScimException if an error occurs.
134   */
135  public ListResponse<SchemaResource> getSchemas()
136      throws ScimException
137  {
138    return searchRequest(SCHEMAS_ENDPOINT).invoke(SchemaResource.class);
139  }
140
141  /**
142   * Retrieve a known schema supported by the service provider.
143   *
144   * @param id The schema URN.
145   * @return The resource type with the provided URN.
146   * @throws ScimException if an error occurs.
147   */
148  public SchemaResource getSchema(final String id)
149      throws ScimException
150  {
151    return retrieve(SCHEMAS_ENDPOINT, id, SchemaResource.class);
152  }
153
154  /**
155   * Create the provided new SCIM resource at the service provider.
156   *
157   * @param endpoint The resource endpoint such as: "{@code Users}" or "Groups" as
158   *                 defined by the associated resource type.
159   * @param resource The new resource to create.
160   * @param <T> The Java type of the resource.
161   * @return The successfully create SCIM resource.
162   * @throws ScimException if an error occurs.
163   */
164  public <T extends ScimResource> T create(
165      final String endpoint, final T resource) throws ScimException
166  {
167    return createRequest(endpoint, resource).invoke();
168  }
169
170  /**
171   * Retrieve a known SCIM resource from the service provider.
172   *
173   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
174   *                 defined by the associated resource type.
175   * @param id The resource identifier (for example the value of the "{@code id}"
176   *           attribute).
177   * @param cls The Java class object used to determine the type to return.
178   * @param <T> The Java type of the resource.
179   * @return The successfully retrieved SCIM resource.
180   * @throws ScimException if an error occurs.
181   */
182  public <T extends ScimResource> T retrieve(
183      final String endpoint, final String id, final Class<T> cls)
184      throws ScimException
185  {
186    return retrieveRequest(endpoint, id).invoke(cls);
187  }
188
189  /**
190   * Retrieve a known SCIM resource from the service provider.
191   *
192   * @param url The URL of the resource to retrieve.
193   * @param cls The Java class object used to determine the type to return.
194   * @param <T> The Java type of the resource.
195   * @return The successfully retrieved SCIM resource.
196   * @throws ScimException if an error occurs.
197   */
198  public <T extends ScimResource> T retrieve(final URI url, final Class<T> cls)
199      throws ScimException
200  {
201    return retrieveRequest(url).invoke(cls);
202  }
203
204  /**
205   * Retrieve a known SCIM resource from the service provider. If the
206   * service provider supports resource versioning and the resource has not been
207   * modified, the provided resource will be returned.
208   *
209   * @param resource The resource to retrieve.
210   * @param <T> The Java type of the resource.
211   * @return The successfully retrieved SCIM resource.
212   * @throws ScimException if an error occurs.
213   */
214  public <T extends ScimResource> T retrieve(final T resource)
215      throws ScimException
216  {
217    RetrieveRequestBuilder.Generic<T> builder = retrieveRequest(resource);
218    return builder.invoke();
219  }
220
221  /**
222   * Modify a SCIM resource by replacing the resource's attributes at the
223   * service provider. If the service provider supports resource versioning,
224   * the resource will only be modified if it has not been modified since it
225   * was retrieved.
226   *
227   * @param resource The previously retrieved and revised resource.
228   * @param <T> The Java type of the resource.
229   * @return The successfully replaced SCIM resource.
230   * @throws ScimException if an error occurs.
231   */
232  public <T extends ScimResource> T replace(
233      final T resource) throws ScimException
234  {
235    ReplaceRequestBuilder<T> builder = replaceRequest(resource);
236    return builder.invoke();
237  }
238
239  /**
240   * Delete a SCIM resource at the service provider.
241   *
242   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
243   *                 defined by the associated resource type.
244   * @param id The resource identifier (for example the value of the "{@code id}"
245   *           attribute).
246   * @throws ScimException if an error occurs.
247   */
248  public void delete(final String endpoint, final String id)
249      throws ScimException
250  {
251    deleteRequest(endpoint, id).invoke();
252  }
253
254  /**
255   * Delete a SCIM resource at the service provider.
256   *
257   * @param url The URL of the resource to delete.
258   * @throws ScimException if an error occurs.
259   */
260  public void delete(final URI url) throws ScimException
261  {
262    deleteRequest(url).invoke();
263  }
264
265  /**
266   * Delete a SCIM resource at the service provider.
267   *
268   * @param resource The resource to delete.
269   * @param <T> The Java type of the resource.
270   * @throws ScimException if an error occurs.
271   */
272  public <T extends ScimResource> void delete(final T resource)
273      throws ScimException
274  {
275    DeleteRequestBuilder builder = deleteRequest(resource);
276    builder.invoke();
277  }
278
279  /**
280   * Build a request to create the provided new SCIM resource at the service
281   * provider.
282   *
283   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
284   *                 defined by the associated resource type.
285   * @param resource The new resource to create.
286   * @param <T> The Java type of the resource.
287   * @return The request builder that may be used to specify additional request
288   * parameters and to invoke the request.
289   */
290  public <T extends ScimResource> CreateRequestBuilder<T> createRequest(
291      final String endpoint, final T resource)
292  {
293    return new CreateRequestBuilder<T>(baseTarget.path(endpoint), resource);
294  }
295
296  /**
297   * Build a request to retrieve a known SCIM resource from the service
298   * provider.
299   *
300   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
301   *                 defined by the associated resource type.
302   * @param id The resource identifier (for example the value of the "{@code id}"
303   *           attribute).
304   * @return The request builder that may be used to specify additional request
305   * parameters and to invoke the request.
306   */
307  public RetrieveRequestBuilder.Typed retrieveRequest(
308      final String endpoint, final String id)
309  {
310    return new RetrieveRequestBuilder.Typed(baseTarget.path(endpoint).path(id));
311  }
312
313  /**
314   * Build a request to retrieve a known SCIM resource from the service
315   * provider.
316   *
317   * @param url The URL of the resource to retrieve.
318   * @return The request builder that may be used to specify additional request
319   * parameters and to invoke the request.
320   */
321  public RetrieveRequestBuilder.Typed retrieveRequest(final URI url)
322  {
323    return new RetrieveRequestBuilder.Typed(resolveWebTarget(url));
324  }
325
326  /**
327   * Build a request to retrieve a known SCIM resource from the service
328   * provider.
329   *
330   * @param resource The resource to retrieve.
331   * @param <T> The Java type of the resource.
332   * @return The request builder that may be used to specify additional request
333   * parameters and to invoke the request.
334   */
335  public <T extends ScimResource> RetrieveRequestBuilder.Generic<T>
336      retrieveRequest(final T resource)
337  {
338    return new RetrieveRequestBuilder.Generic<T>(
339        resolveWebTarget(checkAndGetLocation(resource)), resource);
340  }
341
342  /**
343   * Build a request to query and retrieve resources of a single resource type
344   * from the service provider.
345   *
346   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
347   *                 defined by the associated resource type.
348   * @return The request builder that may be used to specify additional request
349   * parameters and to invoke the request.
350   */
351  public SearchRequestBuilder searchRequest(final String endpoint)
352  {
353    return new SearchRequestBuilder(baseTarget.path(endpoint));
354  }
355
356  /**
357   * Build a request to modify a SCIM resource by replacing the resource's
358   * attributes at the service provider.
359   *
360   * @param uri The URL of the resource to modify.
361   * @param resource The resource to replace.
362   * @param <T> The Java type of the resource.
363   * @return The request builder that may be used to specify additional request
364   * parameters and to invoke the request.
365   */
366  public <T extends ScimResource> ReplaceRequestBuilder<T> replaceRequest(
367      final URI uri, final T resource)
368  {
369    return new ReplaceRequestBuilder<T>(resolveWebTarget(uri), resource);
370  }
371
372  /**
373   * Build a request to modify a SCIM resource by replacing the resource's
374   * attributes at the service provider.
375   *
376   * @param resource The previously retrieved and revised resource.
377   * @param <T> The Java type of the resource.
378   * @return The request builder that may be used to specify additional request
379   * parameters and to invoke the request.
380   */
381  public <T extends ScimResource> ReplaceRequestBuilder<T> replaceRequest(
382      final T resource)
383  {
384    return new ReplaceRequestBuilder<T>(
385        resolveWebTarget(checkAndGetLocation(resource)), resource);
386  }
387
388  /**
389   * {@inheritDoc}
390   */
391  @Override
392  public <T extends ScimResource> T modify(final String endpoint,
393      final String id, final PatchRequest patchRequest, final Class<T> clazz)
394      throws ScimException
395  {
396    ModifyRequestBuilder.Typed requestBuilder = new ModifyRequestBuilder.Typed(
397        baseTarget.path(endpoint).path(id));
398    for(PatchOperation op : patchRequest.getOperations())
399    {
400      requestBuilder.addOperation(op);
401    }
402    return requestBuilder.invoke(clazz);
403  }
404
405  /**
406   * Modify a SCIM resource by updating one or more attributes using a sequence
407   * of operations to "{@code add}", "{@code remove}", or "{@code replace}" values. The service provider
408   * configuration maybe used to discover service provider support for PATCH.
409   *
410   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
411   *                 defined by the associated resource type.
412   * @param id The resource identifier (for example the value of the "{@code id}"
413   *           attribute).
414   * @return The request builder that may be used to specify additional request
415   * parameters and to invoke the request.
416   */
417  public ModifyRequestBuilder.Typed modifyRequest(
418      final String endpoint, final String id)
419  {
420    return new ModifyRequestBuilder.Typed(
421        baseTarget.path(endpoint).path(id));
422  }
423
424
425  /**
426   * Modify a SCIM resource by updating one or more attributes using a sequence
427   * of operations to "{@code add}", "{@code remove}", or "{@code replace}" values. The service provider
428   * configuration maybe used to discover service provider support for PATCH.
429   *
430   * @param url The URL of the resource to modify.
431   * @return The request builder that may be used to specify additional request
432   * parameters and to invoke the request.
433   */
434  public  ModifyRequestBuilder.Typed modifyRequest(
435      final URI url)
436  {
437    return new ModifyRequestBuilder.Typed(resolveWebTarget(url));
438  }
439
440  /**
441   * {@inheritDoc}
442   */
443  @Override
444  public <T extends ScimResource> T modify(
445      final T resource, final PatchRequest patchRequest) throws ScimException
446  {
447    ModifyRequestBuilder.Generic<T> requestBuilder =
448        new ModifyRequestBuilder.Generic<T>(resolveWebTarget(
449            checkAndGetLocation(resource)), resource);
450
451    for(PatchOperation op : patchRequest.getOperations())
452    {
453      requestBuilder.addOperation(op);
454    }
455    return requestBuilder.invoke();
456 }
457
458  /**
459   * Modify a SCIM resource by updating one or more attributes using a sequence
460   * of operations to "{@code add}", "{@code remove}", or "{@code replace}" values. The service provider
461   * configuration maybe used to discover service provider support for PATCH.
462   *
463   * @param resource The resource to modify.
464   * @param <T> The Java type of the resource.
465   * @return The request builder that may be used to specify additional request
466   * parameters and to invoke the request.
467   */
468  public <T extends ScimResource> ModifyRequestBuilder.Generic<T> modifyRequest(
469      final T resource)
470  {
471    return new ModifyRequestBuilder.Generic<T>(
472        resolveWebTarget(checkAndGetLocation(resource)), resource);
473  }
474
475  /**
476   * Build a request to delete a SCIM resource at the service provider.
477   *
478   * @param endpoint The resource endpoint such as: "{@code Users}" or "{@code Groups}" as
479   *                 defined by the associated resource type.
480   * @param id The resource identifier (for example the value of the "{@code id}"
481   *           attribute).
482   * @return The request builder that may be used to specify additional request
483   * parameters and to invoke the request.
484   * @throws ScimException if an error occurs.
485   */
486  public DeleteRequestBuilder deleteRequest(
487      final String endpoint, final String id)
488      throws ScimException
489  {
490    return new DeleteRequestBuilder(baseTarget.path(endpoint).path(id));
491  }
492
493  /**
494   * Build a request to delete a SCIM resource at the service provider.
495   *
496   * @param url The URL of the resource to delete.
497   * @return The request builder that may be used to specify additional request
498   * parameters and to invoke the request.
499   * @throws ScimException if an error occurs.
500   */
501  public DeleteRequestBuilder deleteRequest(final URI url) throws ScimException
502  {
503    return new DeleteRequestBuilder(resolveWebTarget(url));
504  }
505
506  /**
507   * Build a request to delete a SCIM resource at the service provider.
508   *
509   * @param resource The resource to delete.
510   * @param <T> The Java type of the resource.
511   * @return The request builder that may be used to specify additional request
512   * parameters and to invoke the request.
513   * @throws ScimException if an error occurs.
514   */
515  public <T extends ScimResource> DeleteRequestBuilder deleteRequest(
516      final T resource)
517      throws ScimException
518  {
519    return deleteRequest(checkAndGetLocation(resource));
520  }
521
522  /**
523   * Resolve a URL (relative or absolute) to a web target.
524   *
525   * @param url The URL to resolve.
526   * @return The WebTarget.
527   */
528  private WebTarget resolveWebTarget(final URI url)
529  {
530    URI relativePath;
531    if(url.isAbsolute())
532    {
533      relativePath = baseTarget.getUri().relativize(url);
534      if (relativePath.equals(url))
535      {
536        // The given resource's location is from another service provider
537        throw new IllegalArgumentException("Given resource's location " +
538            url + " is not under this service's " +
539            "base path " + baseTarget.getUri());
540      }
541    }
542    else
543    {
544      relativePath = url;
545    }
546
547    return baseTarget.path(relativePath.getRawPath());
548  }
549
550  /**
551   * Get the meta.location attribute value of the SCIM resource.
552   *
553   * @param resource The SCIM resource.
554   * @return The meta.location attribute value.
555   * @throws IllegalArgumentException if the resource does not contain the
556   * meta.location attribute value.
557   */
558  private URI checkAndGetLocation(final ScimResource resource)
559      throws IllegalArgumentException
560  {
561    Meta meta = resource.getMeta();
562    if(meta == null || meta.getLocation() == null)
563    {
564      throw new IllegalArgumentException(
565          "Resource URI must be specified by meta.location");
566    }
567    return meta.getLocation();
568  }
569
570  /**
571   * {@inheritDoc}
572   */
573  public <T extends ScimResource> ListResponse<T> search(final String endpoint,
574      final String filter, final Class<T> clazz) throws ScimException
575  {
576    return searchRequest(endpoint).filter(filter).invoke(clazz);
577  }
578}