001/*
002 * Copyright 2011-2013 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
018package com.unboundid.scim.wink;
019
020import com.unboundid.scim.data.BaseResource;
021import com.unboundid.scim.marshal.Unmarshaller;
022import com.unboundid.scim.marshal.json.JsonUnmarshaller;
023import com.unboundid.scim.marshal.xml.XmlUnmarshaller;
024import com.unboundid.scim.schema.ResourceDescriptor;
025import com.unboundid.scim.sdk.AttributePath;
026import com.unboundid.scim.sdk.Debug;
027import com.unboundid.scim.sdk.DeleteResourceRequest;
028import com.unboundid.scim.sdk.GetResourceRequest;
029import com.unboundid.scim.sdk.GetResourcesRequest;
030import com.unboundid.scim.sdk.OAuthToken;
031import com.unboundid.scim.sdk.OAuthTokenHandler;
032import com.unboundid.scim.sdk.OAuthTokenStatus;
033import com.unboundid.scim.sdk.PageParameters;
034import com.unboundid.scim.sdk.PatchResourceRequest;
035import com.unboundid.scim.sdk.PostResourceRequest;
036import com.unboundid.scim.sdk.PutResourceRequest;
037import com.unboundid.scim.sdk.ResourceNotFoundException;
038import com.unboundid.scim.sdk.ResourceSchemaBackend;
039import com.unboundid.scim.sdk.Resources;
040import com.unboundid.scim.sdk.SCIMBackend;
041import com.unboundid.scim.sdk.SCIMException;
042import com.unboundid.scim.sdk.SCIMFilter;
043import com.unboundid.scim.sdk.SCIMQueryAttributes;
044import com.unboundid.scim.sdk.SCIMRequest;
045import com.unboundid.scim.sdk.SortParameters;
046import com.unboundid.scim.sdk.UnauthorizedException;
047
048import javax.ws.rs.core.HttpHeaders;
049import javax.ws.rs.core.MediaType;
050import javax.ws.rs.core.Response;
051import java.io.InputStream;
052import java.net.URI;
053import java.util.List;
054import java.util.concurrent.atomic.AtomicReference;
055
056import static com.unboundid.scim.sdk.SCIMConstants.
057    HEADER_NAME_ACCESS_CONTROL_ALLOW_CREDENTIALS;
058import static com.unboundid.scim.sdk.SCIMConstants.
059    HEADER_NAME_ACCESS_CONTROL_ALLOW_ORIGIN;
060import static com.unboundid.scim.sdk.SCIMConstants.QUERY_PARAMETER_ATTRIBUTES;
061import static com.unboundid.scim.sdk.SCIMConstants.RESOURCE_ENDPOINT_SCHEMAS;
062
063
064/**
065 * This class is an abstract Wink resource implementation for
066 * SCIM operations on a SCIM endpoint.
067 */
068public abstract class AbstractSCIMResource extends AbstractStaticResource
069{
070  private final SCIMApplication application;
071
072  /**
073   * The OAuth 2.0 bearer token handler. This may be null.
074   */
075  private final OAuthTokenHandler tokenHandler;
076
077  private final ResourceSchemaBackend resourceSchemaBackend;
078
079  /**
080   * Create a new AbstractSCIMResource for CRUD operations.
081   *
082   * @param application         The SCIM JAX-RS application associated with this
083   *                            resource.
084   * @param tokenHandler        The token handler to use for OAuth
085   *                            authentication.
086   */
087  public AbstractSCIMResource(final SCIMApplication application,
088                              final OAuthTokenHandler tokenHandler)
089  {
090    this.application = application;
091    this.tokenHandler = tokenHandler;
092    this.resourceSchemaBackend = new ResourceSchemaBackend(application);
093  }
094
095
096
097  /**
098   * Process a GET operation.
099   *
100   * @param requestContext The request context.
101   * @param endpoint       The endpoint requested.
102   * @param userID         The user ID requested.
103   *
104   * @return  The response to the operation.
105   */
106  Response getUser(final RequestContext requestContext,
107                   final String endpoint, final String userID)
108  {
109    SCIMBackend backend;
110    ResourceDescriptor resourceDescriptor = null;
111    Response.ResponseBuilder responseBuilder;
112    try {
113      backend = getBackend(endpoint);
114      resourceDescriptor = backend.getResourceDescriptor(endpoint);
115      if(resourceDescriptor == null)
116      {
117        throw new ResourceNotFoundException(
118                endpoint + " is not a valid resource endpoint");
119      }
120      String authID = requestContext.getAuthID();
121      if(authID == null && tokenHandler == null) {
122        throw new UnauthorizedException("Invalid credentials");
123      }
124      final String attributes =
125          requestContext.getUriInfo().getQueryParameters().getFirst(
126              QUERY_PARAMETER_ATTRIBUTES);
127      final SCIMQueryAttributes queryAttributes =
128          new SCIMQueryAttributes(resourceDescriptor, attributes);
129
130      // Process the request.
131      GetResourceRequest getResourceRequest =
132          new GetResourceRequest(requestContext.getUriInfo().getBaseUri(),
133              authID, resourceDescriptor, userID, queryAttributes,
134              requestContext.getRequest());
135
136      if (authID == null)
137      {
138        AtomicReference<String> authIDRef = new AtomicReference<String>();
139        Response response = validateOAuthToken(requestContext,
140                              getResourceRequest, authIDRef, tokenHandler);
141        if (response != null)
142        {
143          application.getStatsForResource(resourceDescriptor.getName()).
144              incrementStat("get-" + response.getStatus());
145          return response;
146        }
147        else
148        {
149          authID = authIDRef.get();
150          getResourceRequest =
151               new GetResourceRequest(requestContext.getUriInfo().getBaseUri(),
152                   authID, resourceDescriptor, userID, queryAttributes,
153                   requestContext.getRequest());
154        }
155      }
156
157      BaseResource resource =
158          backend.getResource(getResourceRequest);
159      // Build the response.
160      responseBuilder = Response.status(Response.Status.OK);
161      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
162                        resource);
163      URI location = resource.getMeta().getLocation();
164      if(location != null)
165      {
166        responseBuilder.location(location);
167      }
168
169      if (requestContext.getOrigin() != null)
170      {
171        responseBuilder.header(HEADER_NAME_ACCESS_CONTROL_ALLOW_ORIGIN,
172            requestContext.getOrigin());
173      }
174      responseBuilder.header(HEADER_NAME_ACCESS_CONTROL_ALLOW_CREDENTIALS,
175          Boolean.TRUE.toString());
176
177      application.getStatsForResource(resourceDescriptor.getName()).
178        incrementStat(ResourceStats.GET_OK);
179      if(requestContext.getProduceMediaType() ==
180          MediaType.APPLICATION_JSON_TYPE)
181      {
182        application.getStatsForResource(resourceDescriptor.getName()).
183            incrementStat(ResourceStats.GET_RESPONSE_JSON);
184      }
185      else if(requestContext.getProduceMediaType() ==
186              MediaType.APPLICATION_XML_TYPE)
187      {
188        application.getStatsForResource(resourceDescriptor.getName()).
189            incrementStat(ResourceStats.GET_RESPONSE_XML);
190      }
191    } catch (SCIMException e) {
192      // Build the response.
193      responseBuilder = Response.status(e.getStatusCode());
194      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
195                        e);
196      if(resourceDescriptor != null)
197      {
198        application.getStatsForResource(resourceDescriptor.getName()).
199            incrementStat("get-" + e.getStatusCode());
200      }
201    }
202
203    return responseBuilder.build();
204  }
205
206
207
208  /**
209   * Process a GET operation.
210   *
211   * @param requestContext   The request context.
212   * @param endpoint         The endpoint requested.
213   * @param filterString     The filter query parameter, or {@code null}.
214   * @param baseID           The SCIM resource ID of the search base entry,
215   *                         or {@code null}.
216   * @param searchScope      The LDAP search scope to use, or {@code null}.
217   * @param sortBy           The sortBy query parameter, or {@code null}.
218   * @param sortOrder        The sortOrder query parameter, or {@code null}.
219   * @param pageStartIndex   The startIndex query parameter, or {@code null}.
220   * @param pageSize         The count query parameter, or {@code null}.
221   *
222   * @return  The response to the operation.
223   */
224  protected Response getUsers(final RequestContext requestContext,
225                              final String endpoint,
226                              final String filterString,
227                              final String baseID,
228                              final String searchScope,
229                              final String sortBy,
230                              final String sortOrder,
231                              final String pageStartIndex,
232                              final String pageSize)
233  {
234    SCIMBackend backend;
235    ResourceDescriptor resourceDescriptor = null;
236    Response.ResponseBuilder responseBuilder;
237    try
238    {
239      backend = getBackend(endpoint);
240      resourceDescriptor = backend.getResourceDescriptor(endpoint);
241      if(resourceDescriptor == null)
242      {
243        throw new ResourceNotFoundException(
244                endpoint + " is not a valid resource endpoint");
245      }
246      String authID = requestContext.getAuthID();
247      if(authID == null && tokenHandler == null) {
248        throw new UnauthorizedException("Invalid credentials");
249      }
250      final String attributes =
251          requestContext.getUriInfo().getQueryParameters().getFirst(
252              QUERY_PARAMETER_ATTRIBUTES);
253      final SCIMQueryAttributes queryAttributes =
254          new SCIMQueryAttributes(resourceDescriptor, attributes);
255
256      // Parse the filter parameters.
257      final SCIMFilter filter;
258      if (filterString != null && !filterString.isEmpty())
259      {
260        if(resourceDescriptor.getSchema().equalsIgnoreCase(
261                "urn:unboundid:schemas:scim:ldap:1.0"))
262        {
263          filter = SCIMFilter.parse(
264                  filterString, resourceDescriptor.getSchema());
265        }
266        else
267        {
268          filter = SCIMFilter.parse(filterString);
269        }
270      }
271      else
272      {
273        filter = null;
274      }
275
276      // Parse the sort parameters.
277      final SortParameters sortParameters;
278      if (sortBy != null && !sortBy.isEmpty())
279      {
280        sortParameters =
281            new SortParameters(AttributePath.parse(sortBy,
282                                resourceDescriptor.getSchema()), sortOrder);
283      }
284      else
285      {
286        sortParameters = null;
287      }
288
289      // Parse the pagination parameters.
290      int startIndex = -1;
291      int count = -1;
292      if (pageStartIndex != null && !pageStartIndex.isEmpty())
293      {
294        try
295        {
296          startIndex = Integer.parseInt(pageStartIndex);
297        }
298        catch (NumberFormatException e)
299        {
300          Debug.debugException(e);
301          throw SCIMException.createException(
302              400, "The pagination startIndex value '" + pageStartIndex +
303              "' is not parsable");
304        }
305
306        if (startIndex <= 0)
307        {
308          throw SCIMException.createException(
309              400, "The pagination startIndex value '" + pageStartIndex +
310              "' is invalid because it is not greater than zero");
311        }
312      }
313      if (pageSize != null && !pageSize.isEmpty())
314      {
315        try
316        {
317          count = Integer.parseInt(pageSize);
318        }
319        catch (NumberFormatException e)
320        {
321          Debug.debugException(e);
322          throw SCIMException.createException(
323              400, "The pagination count value '" + pageSize +
324              "' is not parsable");
325        }
326
327        if (count <= 0)
328        {
329          throw SCIMException.createException(
330              400, "The pagination count value '" + pageSize +
331              "' is invalid because it is not greater than zero");
332        }
333      }
334
335      final PageParameters pageParameters;
336      if (startIndex >= 0 && count >= 0)
337      {
338        pageParameters = new PageParameters(startIndex, count);
339      }
340      else if (startIndex >= 0)
341      {
342        pageParameters = new PageParameters(startIndex, 0);
343      }
344      else if (count >= 0)
345      {
346        pageParameters = new PageParameters(1, count);
347      }
348      else
349      {
350        pageParameters = null;
351      }
352
353      // Process the request.
354      GetResourcesRequest getResourcesRequest =
355          new GetResourcesRequest(requestContext.getUriInfo().getBaseUri(),
356              authID, resourceDescriptor, filter, baseID, searchScope,
357              sortParameters, pageParameters, queryAttributes,
358              requestContext.getRequest());
359
360      if (authID == null)
361      {
362        AtomicReference<String> authIDRef = new AtomicReference<String>();
363        Response response = validateOAuthToken(requestContext,
364                              getResourcesRequest, authIDRef, tokenHandler);
365        if (response != null)
366        {
367          application.getStatsForResource(resourceDescriptor.getName()).
368              incrementStat("query-" + response.getStatus());
369          return response;
370        }
371        else
372        {
373          authID = authIDRef.get();
374          getResourcesRequest =
375              new GetResourcesRequest(requestContext.getUriInfo().getBaseUri(),
376                      authID, resourceDescriptor, filter, baseID, searchScope,
377                      sortParameters, pageParameters, queryAttributes,
378                      requestContext.getRequest());
379        }
380      }
381
382      final Resources resources = backend.getResources(getResourcesRequest);
383
384      // Build the response.
385      responseBuilder =
386          Response.status(Response.Status.OK);
387      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
388                        resources);
389
390      if (requestContext.getOrigin() != null)
391      {
392        responseBuilder.header(HEADER_NAME_ACCESS_CONTROL_ALLOW_ORIGIN,
393            requestContext.getOrigin());
394      }
395      responseBuilder.header(HEADER_NAME_ACCESS_CONTROL_ALLOW_CREDENTIALS,
396          Boolean.TRUE.toString());
397
398      application.getStatsForResource(resourceDescriptor.getName()).
399          incrementStat(ResourceStats.QUERY_OK);
400      if(requestContext.getProduceMediaType() ==
401          MediaType.APPLICATION_JSON_TYPE)
402      {
403        application.getStatsForResource(resourceDescriptor.getName()).
404            incrementStat(ResourceStats.QUERY_RESPONSE_JSON);
405      }
406      else if(requestContext.getProduceMediaType() ==
407              MediaType.APPLICATION_XML_TYPE)
408      {
409        application.getStatsForResource(resourceDescriptor.getName()).
410            incrementStat(ResourceStats.QUERY_RESPONSE_XML);
411      }
412    }
413    catch(SCIMException e)
414    {
415      responseBuilder =
416          Response.status(e.getStatusCode());
417      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
418                        e);
419      if(resourceDescriptor != null)
420      {
421        application.getStatsForResource(resourceDescriptor.getName()).
422            incrementStat("query-" + e.getStatusCode());
423      }
424    }
425
426    return responseBuilder.build();
427  }
428
429
430
431  /**
432   * Process a POST operation.
433   *
434   * @param requestContext    The request context.
435   * @param endpoint       The endpoint requested.
436   * @param inputStream       The content to be consumed.
437   *
438   * @return  The response to the operation.
439   */
440  Response postUser(final RequestContext requestContext,
441                    final String endpoint,
442                    final InputStream inputStream)
443  {
444    SCIMBackend backend;
445    ResourceDescriptor resourceDescriptor = null;
446    Response.ResponseBuilder responseBuilder;
447    try
448    {
449      backend = getBackend(endpoint);
450      resourceDescriptor = backend.getResourceDescriptor(endpoint);
451      if(resourceDescriptor == null)
452      {
453        throw new ResourceNotFoundException(
454                endpoint + " is not a valid resource endpoint");
455      }
456      final Unmarshaller unmarshaller;
457      if (requestContext.getConsumeMediaType().equals(
458          MediaType.APPLICATION_JSON_TYPE))
459      {
460        unmarshaller = new JsonUnmarshaller();
461        application.getStatsForResource(resourceDescriptor.getName()).
462            incrementStat(ResourceStats.POST_CONTENT_JSON);
463      }
464      else
465      {
466        unmarshaller = new XmlUnmarshaller();
467        application.getStatsForResource(resourceDescriptor.getName()).
468            incrementStat(ResourceStats.POST_CONTENT_XML);
469      }
470      String authID = requestContext.getAuthID();
471      if(authID == null && tokenHandler == null)
472      {
473        throw new UnauthorizedException("Invalid credentials");
474      }
475
476      // Parse the resource.
477      final BaseResource postedResource = unmarshaller.unmarshal(
478          inputStream, resourceDescriptor, BaseResource.BASE_RESOURCE_FACTORY);
479
480      final String attributes =
481          requestContext.getUriInfo().getQueryParameters().getFirst(
482              QUERY_PARAMETER_ATTRIBUTES);
483      final SCIMQueryAttributes queryAttributes =
484          new SCIMQueryAttributes(resourceDescriptor, attributes);
485
486      // Process the request.
487      PostResourceRequest postResourceRequest =
488          new PostResourceRequest(requestContext.getUriInfo().getBaseUri(),
489              authID, resourceDescriptor, postedResource.getScimObject(),
490              queryAttributes, requestContext.getRequest());
491
492      if (authID == null)
493      {
494        AtomicReference<String> authIDRef = new AtomicReference<String>();
495        Response response = validateOAuthToken(requestContext,
496                              postResourceRequest, authIDRef, tokenHandler);
497        if (response != null)
498        {
499          application.getStatsForResource(resourceDescriptor.getName()).
500              incrementStat("post-" + response.getStatus());
501          return response;
502        }
503        else
504        {
505          authID = authIDRef.get();
506          postResourceRequest =
507              new PostResourceRequest(requestContext.getUriInfo().getBaseUri(),
508                    authID, resourceDescriptor, postedResource.getScimObject(),
509                    queryAttributes, requestContext.getRequest());
510        }
511      }
512
513      final BaseResource resource = backend.postResource(postResourceRequest);
514      // Build the response.
515      responseBuilder = Response.status(Response.Status.CREATED);
516      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
517          resource);
518      responseBuilder.location(resource.getMeta().getLocation());
519      application.getStatsForResource(resourceDescriptor.getName()).
520          incrementStat(ResourceStats.POST_OK);
521      if(requestContext.getProduceMediaType() ==
522          MediaType.APPLICATION_JSON_TYPE)
523      {
524        application.getStatsForResource(resourceDescriptor.getName()).
525            incrementStat(ResourceStats.POST_RESPONSE_JSON);
526      }
527      else if(requestContext.getProduceMediaType() ==
528              MediaType.APPLICATION_XML_TYPE)
529      {
530        application.getStatsForResource(resourceDescriptor.getName()).
531            incrementStat(ResourceStats.POST_RESPONSE_XML);
532      }
533    } catch (SCIMException e) {
534      Debug.debugException(e);
535      // Build the response.
536      responseBuilder = Response.status(e.getStatusCode());
537      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
538                        e);
539      if(resourceDescriptor != null)
540      {
541        application.getStatsForResource(resourceDescriptor.getName()).
542            incrementStat("post-" + e.getStatusCode());
543      }
544    }
545
546    return responseBuilder.build();
547  }
548
549
550
551  /**
552   * Process a PUT operation.
553   *
554   * @param requestContext    The request context.
555   * @param endpoint          The endpoint requested.
556   * @param userID            The target user ID.
557   * @param inputStream       The content to be consumed.
558   *
559   * @return  The response to the operation.
560   */
561  Response putUser(final RequestContext requestContext,
562                   final String endpoint,
563                   final String userID,
564                   final InputStream inputStream)
565  {
566    SCIMBackend backend;
567    ResourceDescriptor resourceDescriptor = null;
568    Response.ResponseBuilder responseBuilder;
569    try {
570      backend = getBackend(endpoint);
571      resourceDescriptor = backend.getResourceDescriptor(endpoint);
572      if(resourceDescriptor == null)
573      {
574        throw new ResourceNotFoundException(
575                endpoint + " is not a valid resource endpoint");
576      }
577      final Unmarshaller unmarshaller;
578      if (requestContext.getConsumeMediaType().equals(
579          MediaType.APPLICATION_JSON_TYPE))
580      {
581        unmarshaller = new JsonUnmarshaller();
582        application.getStatsForResource(resourceDescriptor.getName()).
583            incrementStat(ResourceStats.PUT_CONTENT_JSON);
584      }
585      else
586      {
587        unmarshaller = new XmlUnmarshaller();
588        application.getStatsForResource(resourceDescriptor.getName()).
589            incrementStat(ResourceStats.PUT_CONTENT_XML);
590      }
591      String authID = requestContext.getAuthID();
592      if(authID == null && tokenHandler == null)
593      {
594        throw new UnauthorizedException("Invalid credentials");
595      }
596
597      // Parse the resource.
598      final BaseResource puttedResource = unmarshaller.unmarshal(
599          inputStream, resourceDescriptor, BaseResource.BASE_RESOURCE_FACTORY);
600
601      final String attributes =
602          requestContext.getUriInfo().getQueryParameters().getFirst(
603              QUERY_PARAMETER_ATTRIBUTES);
604      final SCIMQueryAttributes queryAttributes =
605          new SCIMQueryAttributes(resourceDescriptor, attributes);
606
607      // Process the request.
608      PutResourceRequest putResourceRequest =
609          new PutResourceRequest(requestContext.getUriInfo().getBaseUri(),
610              authID, resourceDescriptor, userID,
611              puttedResource.getScimObject(), queryAttributes,
612              requestContext.getRequest());
613
614      if (authID == null)
615      {
616        AtomicReference<String> authIDRef = new AtomicReference<String>();
617        Response response = validateOAuthToken(requestContext,
618                              putResourceRequest, authIDRef, tokenHandler);
619        if (response != null)
620        {
621          application.getStatsForResource(resourceDescriptor.getName()).
622              incrementStat("put-" + response.getStatus());
623          return response;
624        }
625        else
626        {
627          authID = authIDRef.get();
628          putResourceRequest =
629             new PutResourceRequest(requestContext.getUriInfo().getBaseUri(),
630                  authID, resourceDescriptor, userID,
631                  puttedResource.getScimObject(), queryAttributes,
632                  requestContext.getRequest());
633        }
634      }
635
636      final BaseResource scimResponse = backend.putResource(putResourceRequest);
637      // Build the response.
638      responseBuilder = Response.status(Response.Status.OK);
639      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
640                        scimResponse);
641      responseBuilder.location(scimResponse.getMeta().getLocation());
642      application.getStatsForResource(resourceDescriptor.getName()).
643          incrementStat(ResourceStats.PUT_OK);
644      if(requestContext.getProduceMediaType() ==
645          MediaType.APPLICATION_JSON_TYPE)
646      {
647        application.getStatsForResource(resourceDescriptor.getName()).
648            incrementStat(ResourceStats.PUT_RESPONSE_JSON);
649      }
650      else if(requestContext.getProduceMediaType() ==
651              MediaType.APPLICATION_XML_TYPE)
652      {
653        application.getStatsForResource(resourceDescriptor.getName()).
654            incrementStat(ResourceStats.PUT_RESPONSE_XML);
655      }
656    } catch (SCIMException e) {
657      // Build the response.
658      responseBuilder = Response.status(e.getStatusCode());
659      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
660                        e);
661      if(resourceDescriptor != null)
662      {
663        application.getStatsForResource(resourceDescriptor.getName()).
664            incrementStat("put-" + e.getStatusCode());
665      }
666    }
667
668    return responseBuilder.build();
669  }
670
671
672
673  /**
674   * Process a PATCH operation.
675   *
676   * @param requestContext    The request context.
677   * @param endpoint          The endpoint requested.
678   * @param userID            The target user ID.
679   * @param inputStream       The content to be consumed.
680   *
681   * @return  The response to the operation.
682   */
683  Response patchUser(final RequestContext requestContext,
684                     final String endpoint,
685                     final String userID,
686                     final InputStream inputStream)
687  {
688    SCIMBackend backend;
689    ResourceDescriptor resourceDescriptor = null;
690    Response.ResponseBuilder responseBuilder;
691    try {
692      backend = getBackend(endpoint);
693      resourceDescriptor = backend.getResourceDescriptor(endpoint);
694      if(resourceDescriptor == null)
695      {
696        throw new ResourceNotFoundException(
697                endpoint + " is not a valid resource endpoint");
698      }
699      String authID = requestContext.getAuthID();
700      if(authID == null && tokenHandler == null)
701      {
702        throw new UnauthorizedException("Invalid credentials");
703      }
704      final Unmarshaller unmarshaller;
705      if (requestContext.getConsumeMediaType().equals(
706              MediaType.APPLICATION_JSON_TYPE))
707      {
708        unmarshaller = new JsonUnmarshaller();
709        application.getStatsForResource(resourceDescriptor.getName()).
710            incrementStat(ResourceStats.PATCH_CONTENT_JSON);
711      }
712      else
713      {
714        unmarshaller = new XmlUnmarshaller();
715        application.getStatsForResource(resourceDescriptor.getName()).
716            incrementStat(ResourceStats.PATCH_CONTENT_XML);
717      }
718      // Parse the resource.
719      final BaseResource patchedResource = unmarshaller.unmarshal(
720           inputStream, resourceDescriptor, BaseResource.BASE_RESOURCE_FACTORY);
721
722      final String attributes =
723              requestContext.getUriInfo().getQueryParameters().getFirst(
724                      QUERY_PARAMETER_ATTRIBUTES);
725      final SCIMQueryAttributes queryAttributes =
726              new SCIMQueryAttributes(resourceDescriptor, attributes);
727
728      // Process the request.
729      PatchResourceRequest patchResourceRequest =
730              new PatchResourceRequest(requestContext.getUriInfo().getBaseUri(),
731                      authID, resourceDescriptor, userID,
732                      patchedResource.getScimObject(), queryAttributes,
733                      requestContext.getRequest());
734
735      if (authID == null)
736      {
737        AtomicReference<String> authIDRef = new AtomicReference<String>();
738        Response response = validateOAuthToken(requestContext,
739                              patchResourceRequest, authIDRef, tokenHandler);
740        if (response != null)
741        {
742          application.getStatsForResource(resourceDescriptor.getName()).
743              incrementStat("patch-" + response.getStatus());
744          return response;
745        }
746        else
747        {
748          authID = authIDRef.get();
749          patchResourceRequest =
750             new PatchResourceRequest(requestContext.getUriInfo().getBaseUri(),
751                   authID, resourceDescriptor, userID,
752                   patchedResource.getScimObject(), queryAttributes,
753                   requestContext.getRequest());
754        }
755      }
756
757      final BaseResource scimResponse =
758              backend.patchResource(patchResourceRequest);
759
760      // Build the response.
761      if (!queryAttributes.allAttributesRequested())
762      {
763        responseBuilder = Response.status(Response.Status.OK);
764        setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
765                scimResponse);
766      }
767      else
768      {
769        responseBuilder = Response.status(Response.Status.NO_CONTENT);
770      }
771
772      application.getStatsForResource(resourceDescriptor.getName()).
773          incrementStat(ResourceStats.PATCH_OK);
774      if(requestContext.getProduceMediaType() ==
775          MediaType.APPLICATION_JSON_TYPE)
776      {
777        application.getStatsForResource(resourceDescriptor.getName()).
778            incrementStat(ResourceStats.PATCH_RESPONSE_JSON);
779      }
780      else if(requestContext.getProduceMediaType() ==
781              MediaType.APPLICATION_XML_TYPE)
782      {
783        application.getStatsForResource(resourceDescriptor.getName()).
784            incrementStat(ResourceStats.PATCH_RESPONSE_XML);
785      }
786    } catch (SCIMException e) {
787      // Build the response.
788      responseBuilder = Response.status(e.getStatusCode());
789      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
790              e);
791      if(resourceDescriptor != null)
792      {
793        application.getStatsForResource(resourceDescriptor.getName()).
794            incrementStat("patch-" + e.getStatusCode());
795      }
796    }
797
798    return responseBuilder.build();
799  }
800
801
802
803  /**
804   * Process a DELETE operation.
805   *
806   * @param requestContext    The request context.
807   * @param endpoint          The endpoint requested.
808   * @param userID            The target user ID.
809   *
810   * @return  The response to the operation.
811   */
812  Response deleteUser(final RequestContext requestContext,
813                      final String endpoint,
814                      final String userID)
815  {
816    SCIMBackend backend;
817    ResourceDescriptor resourceDescriptor = null;
818    // Process the request.
819    Response.ResponseBuilder responseBuilder;
820    try {
821      backend = getBackend(endpoint);
822      resourceDescriptor = backend.getResourceDescriptor(endpoint);
823      if(resourceDescriptor == null)
824      {
825        throw new ResourceNotFoundException(
826                endpoint + " is not a valid resource endpoint");
827      }
828      String authID = requestContext.getAuthID();
829      if(authID == null && tokenHandler == null)
830      {
831        throw new UnauthorizedException("Invalid credentials");
832      }
833      DeleteResourceRequest deleteResourceRequest =
834        new DeleteResourceRequest(requestContext.getUriInfo().getBaseUri(),
835            authID, resourceDescriptor, userID, requestContext.getRequest());
836
837      if (authID == null)
838      {
839        AtomicReference<String> authIDRef = new AtomicReference<String>();
840        Response response = validateOAuthToken(requestContext,
841                              deleteResourceRequest, authIDRef, tokenHandler);
842        if (response != null)
843        {
844          application.getStatsForResource(resourceDescriptor.getName()).
845              incrementStat("delete-" + response.getStatus());
846          return response;
847        }
848        else
849        {
850          authID = authIDRef.get();
851          deleteResourceRequest =
852             new DeleteResourceRequest(requestContext.getUriInfo().getBaseUri(),
853                   authID, resourceDescriptor, userID,
854                   requestContext.getRequest());
855        }
856      }
857
858      backend.deleteResource(deleteResourceRequest);
859      // Build the response.
860      responseBuilder = Response.status(Response.Status.OK);
861      application.getStatsForResource(resourceDescriptor.getName()).
862          incrementStat(ResourceStats.DELETE_OK);
863    } catch (SCIMException e) {
864      // Build the response.
865      responseBuilder = Response.status(e.getStatusCode());
866      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
867                        e);
868      if(resourceDescriptor != null)
869      {
870        application.getStatsForResource(resourceDescriptor.getName()).
871            incrementStat("delete-" + e.getStatusCode());
872      }
873    }
874
875    return responseBuilder.build();
876  }
877
878  /**
879   * Handles OAuth bearer token validation. This method should only be called if
880   * 1) the request was not previously authenticated with HTTP Basic Auth, and
881   * 2) we have a OAuthTokenHandler available.
882   * <p>
883   * It will call into the OAuthTokenHandler implementation to validate the
884   * bearer token and then do one of two things:
885   * <ul>
886   *   <li>If the token is valid, it will set the output parameter 'authIDRef'
887   *       to the DN of the authorization entry and return null.</li>
888   *   <li>If the token is invalid, it will construct a Http Response with the
889   *       appropriate headers and return it.</li>
890   * </ul>
891   *
892   * @param context      The incoming HTTP request context.
893   * @param request      The unmarshalled SCIM Request for passing into the
894   *                     token handler.
895   * @param authIDRef    An output parameter to contain the DN of the
896   *                     authorization entry.
897   * @param tokenHandlerImpl The OAuthTokenHandler to use.
898   * @return             {@code null} if the token was successfully validated,
899   *                     otherwise a Response instance containing the error
900   *                     information.
901   */
902  static Response validateOAuthToken(final RequestContext context,
903                                     final SCIMRequest request,
904                                     final AtomicReference<String> authIDRef,
905                                     final OAuthTokenHandler tokenHandlerImpl)
906  {
907    HttpHeaders headers = context.getHeaders();
908    List<String> headerList = headers.getRequestHeader("Authorization");
909
910    if (headerList == null || headerList.isEmpty())
911    {
912      //If the client lacks any authentication information, just return 401
913      Response.ResponseBuilder builder = Response.status(401);
914      builder.header("WWW-Authenticate", "Bearer realm=\"SCIM\"");
915      return builder.build();
916
917    }
918    else if (headerList.size() > 1)
919    {
920      return invalidRequest("The Authorization header has too many values",
921              context.getProduceMediaType());
922    }
923
924    String header = headerList.get(0);
925    String[] authorization = header.split(" ");
926    if (authorization.length == 2 &&
927          authorization[0].equalsIgnoreCase("Bearer") &&
928            authorization[1].length() > 0)
929    {
930      try
931      {
932        OAuthToken token = tokenHandlerImpl.decodeOAuthToken(authorization[1]);
933
934        if (token == null)
935        {
936          return invalidRequest("Could not decode the access token",
937                  context.getProduceMediaType());
938        }
939
940        if (!tokenHandlerImpl.isTokenAuthentic(token))
941        {
942          return invalidToken("The access token is not authentic",
943                  context.getProduceMediaType());
944        }
945
946        if (!tokenHandlerImpl.isTokenForThisServer(token))
947        {
948          return invalidToken(
949                  "The access token is not intended for this server",
950                  context.getProduceMediaType());
951        }
952
953        if (tokenHandlerImpl.isTokenExpired(token))
954        {
955          return invalidToken("The access token is expired",
956                  context.getProduceMediaType());
957        }
958
959        OAuthTokenStatus status =
960                tokenHandlerImpl.validateToken(token, request);
961
962        if (status.getErrorCode().equals(
963                OAuthTokenStatus.ErrorCode.INVALID_TOKEN))
964        {
965          String errorDescription = status.getErrorDescription();
966          return invalidToken(errorDescription, context.getProduceMediaType());
967        }
968        else if (status.getErrorCode().equals(
969                OAuthTokenStatus.ErrorCode.INSUFFICIENT_SCOPE))
970        {
971          String errorDescription = status.getErrorDescription();
972          String scope = status.getScope();
973          return insufficientScope(scope, errorDescription,
974                  context.getProduceMediaType());
975        }
976
977        String authID = tokenHandlerImpl.getAuthzDN(token);
978        if (authID == null)
979        {
980          return invalidToken(
981                  "The access token did not contain an authorization DN",
982                  context.getProduceMediaType());
983        }
984        else
985        {
986          authIDRef.set(authID);
987          return null;
988        }
989      }
990      catch(Throwable t)
991      {
992        Debug.debugException(t);
993        return invalidRequest(t.getMessage(), context.getProduceMediaType());
994      }
995    }
996    else if(authorization.length == 2 &&
997              authorization[0].equalsIgnoreCase("Basic") &&
998                authorization[1].length() > 0)
999    {
1000      //The client tried to do Basic Authentication, and since we made it here,
1001      //it failed.
1002      Response.ResponseBuilder builder = Response.status(401);
1003      builder.header("WWW-Authenticate", "Basic realm=\"SCIM\"");
1004      SCIMException exception = new UnauthorizedException(null);
1005      setResponseEntity(builder, context.getProduceMediaType(), exception);
1006      return builder.build();
1007    }
1008    else
1009    {
1010      return invalidRequest("The Authorization header was malformed",
1011              context.getProduceMediaType());
1012    }
1013  }
1014
1015  /**
1016   * Creates an invalid_request Response with the specified error description.
1017   *
1018   * @param errorDescription The description of the validation error.
1019   * @param mediaType The accept-type for SCIMRequest.
1020   * @return a Response instance.
1021   */
1022  private static Response invalidRequest(final String errorDescription,
1023                                         final MediaType mediaType)
1024  {
1025    Response.ResponseBuilder builder = Response.status(400);
1026    String authHeaderValue = "Bearer realm=\"SCIM\", error=\"invalid_request\"";
1027    if (errorDescription != null && !errorDescription.isEmpty())
1028    {
1029      authHeaderValue += ", error_description=\"" + errorDescription + "\"";
1030    }
1031    builder.header("WWW-Authenticate", authHeaderValue);
1032
1033    SCIMException exception =
1034            SCIMException.createException(400, errorDescription);
1035    setResponseEntity(builder, mediaType, exception);
1036
1037    return builder.build();
1038  }
1039
1040  /**
1041   * Creates an invalid_token Response with the specified error description.
1042   *
1043   * @param errorDescription The description of the validation error.
1044   * @param mediaType The accept-type for SCIMRequest.
1045   * @return a Response instance.
1046   */
1047  private static Response invalidToken(final String errorDescription,
1048                                       final MediaType mediaType)
1049  {
1050    Response.ResponseBuilder builder = Response.status(401);
1051    String authHeaderValue = "Bearer realm=\"SCIM\", error=\"invalid_token\"";
1052    if (errorDescription != null && !errorDescription.isEmpty())
1053    {
1054      authHeaderValue += ", error_description=\"" + errorDescription + "\"";
1055    }
1056    builder.header("WWW-Authenticate", authHeaderValue);
1057
1058    SCIMException exception =
1059            SCIMException.createException(401, errorDescription);
1060    setResponseEntity(builder, mediaType, exception);
1061
1062    return builder.build();
1063  }
1064
1065  /**
1066   * Creates an insufficient_scope Response with the specified error
1067   * description and scope.
1068   *
1069   * @param errorDescription The description of the validation error.
1070   * @param scope The OAuth scope required to access the target resource.
1071   * @param mediaType The accept-type for SCIMRequest.
1072   * @return a Response instance.
1073   */
1074  private static Response insufficientScope(final String scope,
1075                                            final String errorDescription,
1076                                            final MediaType mediaType)
1077  {
1078    Response.ResponseBuilder builder = Response.status(403);
1079    String authHeaderValue =
1080            "Bearer realm=\"SCIM\", error=\"insufficient_scope\"";
1081    if (errorDescription != null && !errorDescription.isEmpty())
1082    {
1083      authHeaderValue += ", error_description=\"" + errorDescription + "\"";
1084    }
1085    if (scope != null && !scope.isEmpty())
1086    {
1087      authHeaderValue += ", scope=\"" + scope + "\"";
1088    }
1089    builder.header("WWW-Authenticate", authHeaderValue);
1090
1091    SCIMException exception =
1092            SCIMException.createException(403, errorDescription);
1093    setResponseEntity(builder, mediaType, exception);
1094
1095    return builder.build();
1096  }
1097
1098  /**
1099   * Retrieves the backend that should service the provided endpoint.
1100   *
1101   * @param endpoint The endpoint requested.
1102   * @return The backend that should service the provided endpoint.
1103   */
1104  private SCIMBackend getBackend(final String endpoint)
1105  {
1106    if(endpoint.equals(RESOURCE_ENDPOINT_SCHEMAS))
1107    {
1108      return resourceSchemaBackend;
1109    }
1110
1111    return application.getBackend();
1112  }
1113}