001 /*
002 * Copyright 2011-2012 UnboundID Corp.
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
018 package com.unboundid.scim.sdk;
019
020 import com.unboundid.scim.data.Meta;
021 import com.unboundid.scim.data.ResourceFactory;
022 import com.unboundid.scim.data.BaseResource;
023 import com.unboundid.scim.marshal.Marshaller;
024 import com.unboundid.scim.marshal.Unmarshaller;
025 import com.unboundid.scim.marshal.json.JsonMarshaller;
026 import com.unboundid.scim.marshal.json.JsonUnmarshaller;
027 import com.unboundid.scim.marshal.xml.XmlMarshaller;
028 import com.unboundid.scim.marshal.xml.XmlUnmarshaller;
029 import com.unboundid.scim.schema.ResourceDescriptor;
030
031 import org.apache.http.HttpException;
032 import org.apache.wink.client.ClientAuthenticationException;
033 import org.apache.wink.client.ClientConfigException;
034 import org.apache.wink.client.ClientResponse;
035 import org.apache.wink.client.ClientRuntimeException;
036 import org.apache.wink.client.ClientWebException;
037 import org.apache.wink.client.RestClient;
038
039 import javax.ws.rs.WebApplicationException;
040 import javax.ws.rs.core.MediaType;
041 import javax.ws.rs.core.Response;
042 import javax.ws.rs.core.StreamingOutput;
043 import javax.ws.rs.core.UriBuilder;
044 import java.io.IOException;
045 import java.io.InputStream;
046 import java.io.OutputStream;
047 import java.net.URI;
048 import java.util.List;
049
050 /**
051 * This class represents a SCIM endpoint (ie. Users, Groups, etc.) and handles
052 * all protocol-level interactions with the service provider. It acts as a
053 * helper class for invoking CRUD operations of resources and processing their
054 * results.
055 *
056 * @param <R> The type of resource instances handled by this SCIMEndpoint.
057 */
058 public class SCIMEndpoint<R extends BaseResource>
059 {
060 private final SCIMService scimService;
061 private final ResourceDescriptor resourceDescriptor;
062 private final ResourceFactory<R> resourceFactory;
063 private final Unmarshaller unmarshaller;
064 private final Marshaller marshaller;
065 private final MediaType contentType;
066 private final MediaType acceptType;
067 private final boolean[] overrides = new boolean[3];
068 private final RestClient client;
069
070
071 /**
072 * Create a SCIMEndpoint with the provided information.
073 *
074 * @param scimService The SCIMService to use.
075 * @param restClient The Wink REST client.
076 * @param resourceDescriptor The resource descriptor of this endpoint.
077 * @param resourceFactory The ResourceFactory that should be used to create
078 * resource instances.
079 */
080 SCIMEndpoint(final SCIMService scimService,
081 final RestClient restClient,
082 final ResourceDescriptor resourceDescriptor,
083 final ResourceFactory<R> resourceFactory)
084 {
085 this.scimService = scimService;
086 this.client = restClient;
087 this.resourceDescriptor = resourceDescriptor;
088 this.resourceFactory = resourceFactory;
089 this.contentType = scimService.getContentType();
090 this.acceptType = scimService.getAcceptType();
091 this.overrides[0] = scimService.isOverridePut();
092 this.overrides[1] = scimService.isOverridePatch();
093 this.overrides[2] = scimService.isOverrideDelete();
094
095 if (scimService.getContentType().equals(MediaType.APPLICATION_JSON_TYPE))
096 {
097 this.marshaller = new JsonMarshaller();
098 }
099 else
100 {
101 this.marshaller = new XmlMarshaller();
102 }
103
104 if(scimService.getAcceptType().equals(MediaType.APPLICATION_JSON_TYPE))
105 {
106 this.unmarshaller = new JsonUnmarshaller();
107 }
108 else
109 {
110 this.unmarshaller = new XmlUnmarshaller();
111 }
112 }
113
114
115
116 /**
117 * Constructs a new instance of a resource object which is empty. This
118 * method does not interact with the SCIM service. It creates a local object
119 * that may be provided to the {@link SCIMEndpoint#create} method after the
120 * attributes have been specified.
121 *
122 * @return A new instance of a resource object.
123 */
124 public R newResource()
125 {
126 return resourceFactory.createResource(resourceDescriptor, new SCIMObject());
127 }
128
129 /**
130 * Retrieves a resource instance given the ID.
131 *
132 * @param id The ID of the resource to retrieve.
133 * @return The retrieved resource.
134 * @throws SCIMException If an error occurs.
135 */
136 public R get(final String id)
137 throws SCIMException
138 {
139 return get(id, null, null);
140 }
141
142 /**
143 * Retrieves a resource instance given the ID, only if the current version
144 * has been modified.
145 *
146 * @param id The ID of the resource to retrieve.
147 * @param etag The entity tag that indicates the entry should be returned
148 * only if the entity tag of the current resource is different
149 * from the provided value. A value of <code>null</code> indicates
150 * unconditional return.
151 * @param requestedAttributes The attributes of the resource to retrieve.
152 * @return The retrieved resource or <code>null</code> if the requested
153 * resource has not been modified.
154 * @throws SCIMException If an error occurs.
155 */
156 public R get(final String id, final String etag,
157 final String... requestedAttributes)
158 throws SCIMException
159 {
160 final UriBuilder uriBuilder = UriBuilder.fromUri(scimService.getBaseURL());
161 uriBuilder.path(resourceDescriptor.getEndpoint());
162
163 // The ServiceProviderConfig is a special case where the id is not
164 // specified.
165 if (id != null)
166 {
167 uriBuilder.path(id);
168 }
169
170 URI uri = uriBuilder.build();
171 org.apache.wink.client.Resource clientResource = client.resource(uri);
172 clientResource.accept(acceptType);
173 clientResource.contentType(contentType);
174 addAttributesQuery(clientResource, requestedAttributes);
175
176 if(scimService.getUserAgent() != null)
177 {
178 clientResource.header("User-Agent", scimService.getUserAgent());
179 }
180
181 if(etag != null && !etag.isEmpty())
182 {
183 clientResource.header("If-None-Match", etag);
184 }
185
186 InputStream entity = null;
187 try
188 {
189 ClientResponse response = clientResource.get();
190 entity = response.getEntity(InputStream.class);
191
192 if(response.getStatusType() == Response.Status.NOT_MODIFIED)
193 {
194 return null;
195 }
196 else if(response.getStatusType() == Response.Status.OK)
197 {
198 R resource = unmarshaller.unmarshal(entity, resourceDescriptor,
199 resourceFactory);
200 addMissingMetaData(response, resource);
201 return resource;
202 }
203 else
204 {
205 throw createErrorResponseException(response, entity);
206 }
207 }
208 catch(SCIMException e)
209 {
210 throw e;
211 }
212 catch(Exception e)
213 {
214 throw SCIMException.createException(getStatusCode(e), e.getMessage());
215 }
216 finally
217 {
218 try {
219 if (entity != null) {
220 entity.close();
221 }
222 } catch (IOException e) {
223 // Lets just log this and ignore.
224 Debug.debugException(e);
225 }
226 }
227 }
228
229 /**
230 * Retrieves all resource instances that match the provided filter.
231 *
232 * @param filter The filter that should be used.
233 * @return The resource instances that match the provided filter.
234 * @throws SCIMException If an error occurs.
235 */
236 public Resources<R> query(final String filter)
237 throws SCIMException
238 {
239 return query(filter, null, null, null);
240 }
241
242 /**
243 * Retrieves all resource instances that match the provided filter.
244 * Matching resources are returned sorted according to the provided
245 * SortParameters. PageParameters maybe used to specify the range of
246 * resource instances that are returned.
247 *
248 * @param filter The filter that should be used.
249 * @param sortParameters The sort parameters that should be used.
250 * @param pageParameters The page parameters that should be used.
251 * @param requestedAttributes The attributes of the resource to retrieve.
252 * @return The resource instances that match the provided filter.
253 * @throws SCIMException If an error occurs.
254 */
255 public Resources<R> query(final String filter,
256 final SortParameters sortParameters,
257 final PageParameters pageParameters,
258 final String... requestedAttributes)
259 throws SCIMException
260 {
261 URI uri =
262 UriBuilder.fromUri(scimService.getBaseURL()).path(
263 resourceDescriptor.getEndpoint()).build();
264 org.apache.wink.client.Resource clientResource = client.resource(uri);
265 clientResource.accept(acceptType);
266 clientResource.contentType(contentType);
267 addAttributesQuery(clientResource, requestedAttributes);
268 if(scimService.getUserAgent() != null)
269 {
270 clientResource.header("User-Agent", scimService.getUserAgent());
271 }
272 if(filter != null)
273 {
274 clientResource.queryParam("filter", filter);
275 }
276 if(sortParameters != null)
277 {
278 clientResource.queryParam("sortBy",
279 sortParameters.getSortBy().toString());
280 if(!sortParameters.isAscendingOrder())
281 {
282 clientResource.queryParam("sortOrder", sortParameters.getSortOrder());
283 }
284 }
285 if(pageParameters != null)
286 {
287 clientResource.queryParam("startIndex",
288 String.valueOf(pageParameters.getStartIndex()));
289 if (pageParameters.getCount() > 0)
290 {
291 clientResource.queryParam("count",
292 String.valueOf(pageParameters.getCount()));
293 }
294 }
295
296 InputStream entity = null;
297 try
298 {
299 ClientResponse response = clientResource.get();
300 entity = response.getEntity(InputStream.class);
301
302 if(response.getStatusType() == Response.Status.OK)
303 {
304 return unmarshaller.unmarshalResources(entity, resourceDescriptor,
305 resourceFactory);
306 }
307 else
308 {
309 throw createErrorResponseException(response, entity);
310 }
311 }
312 catch(SCIMException e)
313 {
314 throw e;
315 }
316 catch(Exception e)
317 {
318 throw SCIMException.createException(getStatusCode(e), e.getMessage());
319 }
320 finally
321 {
322 try {
323 if (entity != null) {
324 entity.close();
325 }
326 } catch (IOException e) {
327 // Lets just log this and ignore.
328 Debug.debugException(e);
329 }
330 }
331 }
332
333
334
335 /**
336 * Create the specified resource instance at the service provider.
337 *
338 * @param resource The resource to create.
339 * @return The newly inserted resource returned by the service provider.
340 * @throws SCIMException If an error occurs.
341 */
342 public R create(final R resource) throws SCIMException
343 {
344 return create(resource, null);
345 }
346
347 /**
348 * Create the specified resource instance at the service provider and return
349 * only the specified attributes from the newly inserted resource.
350 *
351 * @param resource The resource to create.
352 * @param requestedAttributes The attributes of the newly inserted resource
353 * to retrieve.
354 * @return The newly inserted resource returned by the service provider.
355 * @throws SCIMException If an error occurs.
356 */
357 public R create(final R resource,
358 final String... requestedAttributes)
359 throws SCIMException
360 {
361
362 URI uri =
363 UriBuilder.fromUri(scimService.getBaseURL()).path(
364 resourceDescriptor.getEndpoint()).build();
365 org.apache.wink.client.Resource clientResource = client.resource(uri);
366 clientResource.accept(acceptType);
367 clientResource.contentType(contentType);
368 addAttributesQuery(clientResource, requestedAttributes);
369 if(scimService.getUserAgent() != null)
370 {
371 clientResource.header("User-Agent", scimService.getUserAgent());
372 }
373
374 StreamingOutput output = new StreamingOutput() {
375 public void write(final OutputStream outputStream)
376 throws IOException, WebApplicationException {
377 try {
378 marshaller.marshal(resource, outputStream);
379 } catch (Exception e) {
380 throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
381 }
382 }
383 };
384
385
386 InputStream entity = null;
387 try
388 {
389 ClientResponse response = clientResource.post(output);
390 entity = response.getEntity(InputStream.class);
391
392 if(response.getStatusType() == Response.Status.CREATED)
393 {
394 R postedResource = unmarshaller.unmarshal(entity, resourceDescriptor,
395 resourceFactory);
396 addMissingMetaData(response, postedResource);
397 return postedResource;
398 }
399 else
400 {
401 throw createErrorResponseException(response, entity);
402 }
403 }
404 catch(SCIMException e)
405 {
406 throw e;
407 }
408 catch(Exception e)
409 {
410 throw SCIMException.createException(getStatusCode(e), e.getMessage());
411 }
412 finally
413 {
414 try {
415 if (entity != null) {
416 entity.close();
417 }
418 } catch (IOException e) {
419 // Lets just log this and ignore.
420 Debug.debugException(e);
421 }
422 }
423 }
424
425 /**
426 * Update the existing resource with the one provided.
427 *
428 * @param resource The modified resource to be updated.
429 * @return The updated resource returned by the service provider.
430 * @throws SCIMException If an error occurs.
431 */
432 public R update(final R resource)
433 throws SCIMException
434 {
435 return update(resource, null, null);
436 }
437
438 /**
439 * Update the existing resource with the one provided. This update is
440 * conditional upon the provided entity tag matching the tag from the
441 * current resource. If (and only if) they match, the update will be
442 * performed.
443 *
444 * @param resource The modified resource to be updated.
445 * @param etag The entity tag value that is the expected value for the target
446 * resource. A value of <code>null</code> will not set an
447 * etag precondition and a value of "*" will perform an
448 * unconditional update.
449 * @param requestedAttributes The attributes of updated resource
450 * to return.
451 * @return The updated resource returned by the service provider.
452 * @throws SCIMException If an error occurs.
453 */
454 public R update(final R resource, final String etag,
455 final String... requestedAttributes)
456 throws SCIMException
457 {
458 String id = resource.getId();
459 if(id == null)
460 {
461 throw new InvalidResourceException("Resource must have a valid ID");
462 }
463 URI uri =
464 UriBuilder.fromUri(scimService.getBaseURL()).path(
465 resourceDescriptor.getEndpoint()).path(id).build();
466 org.apache.wink.client.Resource clientResource = client.resource(uri);
467 clientResource.accept(acceptType);
468 clientResource.contentType(contentType);
469 addAttributesQuery(clientResource, requestedAttributes);
470 if(scimService.getUserAgent() != null)
471 {
472 clientResource.header("User-Agent", scimService.getUserAgent());
473 }
474 if(etag != null && !etag.isEmpty())
475 {
476 clientResource.header("If-Match", etag);
477 }
478
479 StreamingOutput output = new StreamingOutput() {
480 public void write(final OutputStream outputStream)
481 throws IOException, WebApplicationException {
482 try {
483 marshaller.marshal(resource, outputStream);
484 } catch (Exception e) {
485 throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
486 }
487 }
488 };
489
490 InputStream entity = null;
491 try
492 {
493 ClientResponse response;
494 if(overrides[0])
495 {
496 clientResource.header("X-HTTP-Method-Override", "PUT");
497 response = clientResource.post(output);
498 }
499 else
500 {
501 response = clientResource.put(output);
502 }
503
504 entity = response.getEntity(InputStream.class);
505
506 if(response.getStatusType() == Response.Status.OK)
507 {
508 R postedResource = unmarshaller.unmarshal(entity, resourceDescriptor,
509 resourceFactory);
510 addMissingMetaData(response, postedResource);
511 return postedResource;
512 }
513 else
514 {
515 throw createErrorResponseException(response, entity);
516 }
517 }
518 catch(SCIMException e)
519 {
520 throw e;
521 }
522 catch(Exception e)
523 {
524 throw SCIMException.createException(getStatusCode(e), e.getMessage());
525 }
526 finally
527 {
528 try {
529 if (entity != null) {
530 entity.close();
531 }
532 } catch (IOException e) {
533 // Lets just log this and ignore.
534 Debug.debugException(e);
535 }
536 }
537 }
538
539 /**
540 * Delete the resource instance specified by the provided ID.
541 *
542 * @param id The ID of the resource to delete.
543 * @throws SCIMException If an error occurs.
544 */
545 public void delete(final String id)
546 throws SCIMException
547 {
548 delete(id, null);
549 }
550
551 /**
552 * Delete the resource instance specified by the provided ID. This delete is
553 * conditional upon the provided entity tag matching the tag from the
554 * current resource. If (and only if) they match, the delete will be
555 * performed.
556 *
557 * @param id The ID of the resource to delete.
558 * @param etag The entity tag value that is the expected value for the target
559 * resource. A value of <code>null</code> will not set an
560 * etag precondition and a value of "*" will perform an
561 * unconditional delete.
562 * @throws SCIMException If an error occurs.
563 */
564 public void delete(final String id, final String etag)
565 throws SCIMException
566 {
567 URI uri =
568 UriBuilder.fromUri(scimService.getBaseURL()).path(
569 resourceDescriptor.getEndpoint()).path(id).build();
570 org.apache.wink.client.Resource clientResource = client.resource(uri);
571 clientResource.accept(acceptType);
572 clientResource.contentType(contentType);
573 if(scimService.getUserAgent() != null)
574 {
575 clientResource.header("User-Agent", scimService.getUserAgent());
576 }
577 if(etag != null && !etag.isEmpty())
578 {
579 clientResource.header("If-Match", etag);
580 }
581
582
583 InputStream entity = null;
584 try
585 {
586 ClientResponse response;
587 if(overrides[2])
588 {
589 clientResource.header("X-HTTP-Method-Override", "DELETE");
590 response = clientResource.post(null);
591 }
592 else
593 {
594 response = clientResource.delete();
595 }
596
597 entity = response.getEntity(InputStream.class);
598
599 if(response.getStatusType() != Response.Status.OK)
600 {
601 throw createErrorResponseException(response, entity);
602 }
603 }
604 catch(SCIMException e)
605 {
606 throw e;
607 }
608 catch(Exception e)
609 {
610 throw SCIMException.createException(getStatusCode(e), e.getMessage());
611 }
612 finally
613 {
614 try {
615 if (entity != null) {
616 entity.close();
617 }
618 } catch (IOException e) {
619 // Lets just log this and ignore.
620 Debug.debugException(e);
621 }
622 }
623 }
624
625 /**
626 * Add the attributes query parameter to the client resource request.
627 *
628 * @param clientResource The Wink client resource.
629 * @param requestedAttributes The SCIM attributes to request.
630 */
631 private void addAttributesQuery(
632 final org.apache.wink.client.Resource clientResource,
633 final String... requestedAttributes)
634 {
635 if(requestedAttributes != null && requestedAttributes.length > 0)
636 {
637 StringBuilder stringBuilder = new StringBuilder();
638 for(int i = 0; i < requestedAttributes.length; i++)
639 {
640 stringBuilder.append(requestedAttributes[i]);
641 if(i < requestedAttributes.length - 1)
642 {
643 stringBuilder.append(",");
644 }
645 }
646 clientResource.queryParam("attributes", stringBuilder.toString());
647 }
648 }
649
650 /**
651 * Add meta values from the response header to the meta complex attribute
652 * if they are missing.
653 *
654 * @param response The response from the service provider.
655 * @param resource The return resource instance.
656 */
657 private void addMissingMetaData(final ClientResponse response,
658 final R resource)
659 {
660 URI headerLocation = null;
661 String headerEtag = null;
662 List<String> values = response.getHeaders().get("Location");
663 if(values != null && !values.isEmpty())
664 {
665 headerLocation = URI.create(values.get(0));
666 }
667 values = response.getHeaders().get("Etag");
668 if(values != null && !values.isEmpty())
669 {
670 headerEtag = values.get(0);
671 }
672 Meta meta = resource.getMeta();
673 if(meta == null)
674 {
675 meta = new Meta(null, null, null, null);
676 }
677 boolean modified = false;
678 if(headerLocation != null && meta.getLocation() == null)
679 {
680 meta.setLocation(headerLocation);
681 modified = true;
682 }
683 if(headerEtag != null && meta.getVersion() == null)
684 {
685 meta.setVersion(headerEtag);
686 modified = true;
687 }
688 if(modified)
689 {
690 resource.setMeta(meta);
691 }
692 }
693
694
695
696 /**
697 * Returns a SCIM exception representing the error response.
698 *
699 * @param response The client response.
700 * @param entity The response content.
701 *
702 * @return The SCIM exception representing the error response.
703 */
704 private SCIMException createErrorResponseException(
705 final ClientResponse response,
706 final InputStream entity)
707 {
708 SCIMException scimException = null;
709
710 if(entity != null)
711 {
712 try
713 {
714 scimException = unmarshaller.unmarshalError(entity);
715 }
716 catch (InvalidResourceException e)
717 {
718 // The response content could not be parsed as a SCIM error
719 // response, which is the case if the response is a more general
720 // HTTP error. It is better to just provide the HTTP response
721 // details in this case.
722 Debug.debugException(e);
723 }
724 }
725
726 if(scimException == null)
727 {
728 scimException = SCIMException.createException(
729 response.getStatusCode(), response.getMessage());
730 }
731
732 return scimException;
733 }
734
735
736 /**
737 * Tries to deduce the most appropriate HTTP response code from the given
738 * exception. This method expects most exceptions to be one of 3 or 4
739 * expected runtime exceptions that are common to Wink.
740 *
741 * @param e the Exception instance to analyze
742 * @return the most appropriate HTTP status code
743 */
744 private static int getStatusCode(final Exception e)
745 {
746 if(e instanceof ClientWebException)
747 {
748 ClientWebException cwe = (ClientWebException) e;
749 return cwe.getResponse().getStatusCode();
750 }
751 else if(e instanceof ClientAuthenticationException)
752 {
753 return 401;
754 }
755 else if(e instanceof ClientConfigException)
756 {
757 return 400;
758 }
759 else if(e instanceof ClientRuntimeException)
760 {
761 Throwable t = e.getCause();
762 if(t instanceof HttpException)
763 {
764 return 501;
765 }
766 else if(t instanceof IOException)
767 {
768 return 500;
769 }
770 }
771
772 //Default
773 return 500;
774 }
775 }