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.wink;
019    
020    import com.unboundid.scim.data.BaseResource;
021    import com.unboundid.scim.marshal.Marshaller;
022    import com.unboundid.scim.marshal.Unmarshaller;
023    import com.unboundid.scim.marshal.json.JsonMarshaller;
024    import com.unboundid.scim.marshal.json.JsonUnmarshaller;
025    import com.unboundid.scim.marshal.xml.XmlMarshaller;
026    import com.unboundid.scim.marshal.xml.XmlUnmarshaller;
027    import com.unboundid.scim.schema.ResourceDescriptor;
028    import com.unboundid.scim.sdk.AttributePath;
029    import com.unboundid.scim.sdk.Debug;
030    import com.unboundid.scim.sdk.DeleteResourceRequest;
031    import com.unboundid.scim.sdk.GetResourceRequest;
032    import com.unboundid.scim.sdk.GetResourcesRequest;
033    import com.unboundid.scim.sdk.PageParameters;
034    import com.unboundid.scim.sdk.PostResourceRequest;
035    import com.unboundid.scim.sdk.PutResourceRequest;
036    import com.unboundid.scim.sdk.Resources;
037    import com.unboundid.scim.sdk.SCIMBackend;
038    import com.unboundid.scim.sdk.SCIMException;
039    import com.unboundid.scim.sdk.SCIMFilter;
040    import com.unboundid.scim.sdk.SCIMQueryAttributes;
041    import com.unboundid.scim.sdk.SCIMResponse;
042    import com.unboundid.scim.sdk.SortParameters;
043    import com.unboundid.scim.sdk.UnauthorizedException;
044    import org.apache.wink.common.AbstractDynamicResource;
045    
046    import javax.ws.rs.WebApplicationException;
047    import javax.ws.rs.core.MediaType;
048    import javax.ws.rs.core.Response;
049    import javax.ws.rs.core.StreamingOutput;
050    import java.io.IOException;
051    import java.io.InputStream;
052    import java.io.OutputStream;
053    import java.net.URI;
054    
055    import static com.unboundid.scim.sdk.SCIMConstants.
056        HEADER_NAME_ACCESS_CONTROL_ALLOW_CREDENTIALS;
057    import static com.unboundid.scim.sdk.SCIMConstants.
058        HEADER_NAME_ACCESS_CONTROL_ALLOW_ORIGIN;
059    import static com.unboundid.scim.sdk.SCIMConstants.QUERY_PARAMETER_ATTRIBUTES;
060    
061    
062    
063    /**
064     * This class is an abstract Wink dynamic resource implementation for
065     * SCIM operations on a SCIM endpoint. The set of supported resources and their
066     * endpoints are not known until run-time hence it must be implemented as a
067     * dynamic resource.
068     */
069    public abstract class AbstractSCIMResource extends AbstractDynamicResource
070    {
071      /**
072       * The ResourceDescriptor for this resource.
073       */
074      private final ResourceDescriptor resourceDescriptor;
075    
076      /**
077       * The ResourceStats used to keep activity statistics.
078       */
079      private final ResourceStats resourceStats;
080    
081      /**
082       * The SCIMBackend to use to process requests.
083       */
084      private final SCIMBackend backend;
085    
086      /**
087       * Create a new AbstractSCIMResource for CRUD operations.
088       *
089       * @param path                The path of this resource.
090       * @param resourceDescriptor  The resource descriptor to use.
091       * @param resourceStats       The ResourceStats instance to use.
092       * @param backend             The SCIMBackend to use to process requests.
093       */
094      public AbstractSCIMResource(final String path,
095                                  final ResourceDescriptor resourceDescriptor,
096                                  final ResourceStats resourceStats,
097                                  final SCIMBackend backend)
098      {
099        this.resourceDescriptor = resourceDescriptor;
100        this.backend = backend;
101        this.resourceStats = resourceStats;
102        super.setPath(path);
103      }
104    
105    
106    
107      /**
108       * Process a GET operation.
109       *
110       * @param requestContext The request context.
111       * @param userID         The user ID requested.
112       *
113       * @return  The response to the operation.
114       */
115      Response getUser(final RequestContext requestContext, final String userID)
116      {
117        Response.ResponseBuilder responseBuilder;
118        try {
119          final String authID = requestContext.getAuthID();
120          if(authID == null) {
121            throw new UnauthorizedException("Invalid credentials");
122          }
123          final String attributes =
124              requestContext.getUriInfo().getQueryParameters().getFirst(
125                  QUERY_PARAMETER_ATTRIBUTES);
126          final SCIMQueryAttributes queryAttributes =
127              new SCIMQueryAttributes(resourceDescriptor, attributes);
128    
129          // Process the request.
130          final GetResourceRequest getResourceRequest =
131              new GetResourceRequest(requestContext.getUriInfo().getBaseUri(),
132                  authID, resourceDescriptor, userID, queryAttributes);
133    
134          BaseResource resource =
135              backend.getResource(getResourceRequest);
136          // Build the response.
137          responseBuilder = Response.status(Response.Status.OK);
138          setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
139                            resource);
140          URI location = resource.getMeta().getLocation();
141          if(location != null)
142          {
143            responseBuilder.location(location);
144          }
145          resourceStats.incrementStat(ResourceStats.GET_OK);
146        } catch (SCIMException e) {
147          // Build the response.
148          responseBuilder = Response.status(e.getStatusCode());
149          setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
150                            e);
151          resourceStats.incrementStat("get-" + e.getStatusCode());
152        }
153    
154        if (requestContext.getOrigin() != null)
155        {
156          responseBuilder.header(HEADER_NAME_ACCESS_CONTROL_ALLOW_ORIGIN,
157              requestContext.getOrigin());
158        }
159        responseBuilder.header(HEADER_NAME_ACCESS_CONTROL_ALLOW_CREDENTIALS,
160            Boolean.TRUE.toString());
161    
162        if(requestContext.getProduceMediaType() == MediaType.APPLICATION_JSON_TYPE)
163        {
164          resourceStats.incrementStat(ResourceStats.GET_RESPONSE_JSON);
165        }
166        else if(requestContext.getProduceMediaType() ==
167                MediaType.APPLICATION_XML_TYPE)
168        {
169          resourceStats.incrementStat(ResourceStats.GET_RESPONSE_XML);
170        }
171    
172        return responseBuilder.build();
173      }
174    
175    
176    
177      /**
178       * Process a GET operation.
179       *
180       * @param requestContext   The request context.
181       * @param filterString     The filter query parameter, or {@code null}.
182       * @param sortBy           The sortBy query parameter, or {@code null}.
183       * @param sortOrder        The sortOrder query parameter, or {@code null}.
184       * @param pageStartIndex   The startIndex query parameter, or {@code null}.
185       * @param pageSize         The count query parameter, or {@code null}.
186       *
187       * @return  The response to the operation.
188       */
189      protected Response getUsers(final RequestContext requestContext,
190                                  final String filterString,
191                                  final String sortBy,
192                                  final String sortOrder,
193                                  final String pageStartIndex,
194                                  final String pageSize)
195      {
196        Response.ResponseBuilder responseBuilder;
197        try
198        {
199          String authID = requestContext.getAuthID();
200          if(authID == null) {
201            throw new UnauthorizedException("Invalid credentials");
202          }
203          final String attributes =
204              requestContext.getUriInfo().getQueryParameters().getFirst(
205                  QUERY_PARAMETER_ATTRIBUTES);
206          final SCIMQueryAttributes queryAttributes =
207              new SCIMQueryAttributes(resourceDescriptor, attributes);
208    
209          // Parse the filter parameters.
210          final SCIMFilter filter;
211          if (filterString != null && !filterString.isEmpty())
212          {
213            filter = SCIMFilter.parse(filterString);
214          }
215          else
216          {
217            filter = null;
218          }
219    
220          // Parse the sort parameters.
221          final SortParameters sortParameters;
222          if (sortBy != null && !sortBy.isEmpty())
223          {
224            sortParameters =
225                new SortParameters(AttributePath.parse(sortBy), sortOrder);
226          }
227          else
228          {
229            sortParameters = null;
230          }
231    
232          // Parse the pagination parameters.
233          long startIndex = -1;
234          int count = -1;
235          if (pageStartIndex != null && !pageStartIndex.isEmpty())
236          {
237            try
238            {
239              startIndex = Long.parseLong(pageStartIndex);
240            }
241            catch (NumberFormatException e)
242            {
243              Debug.debugException(e);
244              throw SCIMException.createException(
245                  400, "The pagination startIndex value '" + pageStartIndex +
246                  "' is not parsable");
247            }
248    
249            if (startIndex <= 0)
250            {
251              throw SCIMException.createException(
252                  400, "The pagination startIndex value '" + pageStartIndex +
253                  "' is invalid because it is not greater than zero");
254            }
255          }
256          if (pageSize != null && !pageSize.isEmpty())
257          {
258            try
259            {
260              count = Integer.parseInt(pageSize);
261            }
262            catch (NumberFormatException e)
263            {
264              Debug.debugException(e);
265              throw SCIMException.createException(
266                  400, "The pagination count value '" + pageSize +
267                  "' is not parsable");
268            }
269    
270            if (count <= 0)
271            {
272              throw SCIMException.createException(
273                  400, "The pagination count value '" + pageSize +
274                  "' is invalid because it is not greater than zero");
275            }
276          }
277    
278          final PageParameters pageParameters;
279          if (startIndex >= 0 && count >= 0)
280          {
281            pageParameters = new PageParameters(startIndex, count);
282          }
283          else if (startIndex >= 0)
284          {
285            pageParameters = new PageParameters(startIndex, 0);
286          }
287          else if (count >= 0)
288          {
289            pageParameters = new PageParameters(1, count);
290          }
291          else
292          {
293            pageParameters = null;
294          }
295    
296          // Process the request.
297          final GetResourcesRequest getResourcesRequest =
298              new GetResourcesRequest(requestContext.getUriInfo().getBaseUri(),
299                  authID, resourceDescriptor, filter, sortParameters,
300                  pageParameters, queryAttributes);
301    
302    
303          final Resources resources = backend.getResources(getResourcesRequest);
304    
305          // Build the response.
306          responseBuilder =
307              Response.status(Response.Status.OK);
308          setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
309                            resources);
310          resourceStats.incrementStat(ResourceStats.QUERY_OK);
311        }
312        catch(SCIMException e)
313        {
314          responseBuilder =
315              Response.status(e.getStatusCode());
316          setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
317                            e);
318          resourceStats.incrementStat("query-" + e.getStatusCode());
319        }
320    
321        if (requestContext.getOrigin() != null)
322        {
323          responseBuilder.header(HEADER_NAME_ACCESS_CONTROL_ALLOW_ORIGIN,
324              requestContext.getOrigin());
325        }
326        responseBuilder.header(HEADER_NAME_ACCESS_CONTROL_ALLOW_CREDENTIALS,
327            Boolean.TRUE.toString());
328    
329        if(requestContext.getProduceMediaType() == MediaType.APPLICATION_JSON_TYPE)
330        {
331          resourceStats.incrementStat(ResourceStats.QUERY_RESPONSE_JSON);
332        }
333        else if(requestContext.getProduceMediaType() ==
334                MediaType.APPLICATION_XML_TYPE)
335        {
336          resourceStats.incrementStat(ResourceStats.QUERY_RESPONSE_XML);
337        }
338    
339        return responseBuilder.build();
340      }
341    
342    
343    
344      /**
345       * Process a POST operation.
346       *
347       * @param requestContext    The request context.
348       * @param inputStream       The content to be consumed.
349       *
350       * @return  The response to the operation.
351       */
352      Response postUser(final RequestContext requestContext,
353                                final InputStream inputStream)
354      {
355        final Unmarshaller unmarshaller;
356        if (requestContext.getConsumeMediaType().equals(
357            MediaType.APPLICATION_JSON_TYPE))
358        {
359          unmarshaller = new JsonUnmarshaller();
360          resourceStats.incrementStat(ResourceStats.POST_CONTENT_JSON);
361        }
362        else
363        {
364          unmarshaller = new XmlUnmarshaller();
365          resourceStats.incrementStat(ResourceStats.POST_CONTENT_XML);
366        }
367    
368        Response.ResponseBuilder responseBuilder;
369        try
370        {
371          String authID = requestContext.getAuthID();
372          if(authID == null) {
373            throw new UnauthorizedException("Invalid credentials");
374          }
375    
376          // Parse the resource.
377          final BaseResource postedResource = unmarshaller.unmarshal(
378              inputStream, resourceDescriptor, BaseResource.BASE_RESOURCE_FACTORY);
379    
380          final String attributes =
381              requestContext.getUriInfo().getQueryParameters().getFirst(
382                  QUERY_PARAMETER_ATTRIBUTES);
383          final SCIMQueryAttributes queryAttributes =
384              new SCIMQueryAttributes(resourceDescriptor, attributes);
385    
386          // Process the request.
387          final PostResourceRequest postResourceRequest =
388              new PostResourceRequest(requestContext.getUriInfo().getBaseUri(),
389                  authID, resourceDescriptor, postedResource.getScimObject(),
390                  queryAttributes);
391    
392          final BaseResource resource =
393              backend.postResource(postResourceRequest);
394          // Build the response.
395          responseBuilder = Response.status(Response.Status.CREATED);
396          setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
397              resource);
398          responseBuilder.location(resource.getMeta().getLocation());
399          resourceStats.incrementStat(ResourceStats.POST_OK);
400        } catch (SCIMException e) {
401          Debug.debugException(e);
402          // Build the response.
403          responseBuilder = Response.status(e.getStatusCode());
404          setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
405                            e);
406          resourceStats.incrementStat("post-" + e.getStatusCode());
407        }
408    
409        if(requestContext.getProduceMediaType() == MediaType.APPLICATION_JSON_TYPE)
410        {
411          resourceStats.incrementStat(ResourceStats.POST_RESPONSE_JSON);
412        }
413        else if(requestContext.getProduceMediaType() ==
414                MediaType.APPLICATION_XML_TYPE)
415        {
416          resourceStats.incrementStat(ResourceStats.POST_RESPONSE_XML);
417        }
418    
419        return responseBuilder.build();
420      }
421    
422    
423    
424      /**
425       * Process a PUT operation.
426       *
427       * @param requestContext    The request context.
428       * @param userID            The target user ID.
429       * @param inputStream       The content to be consumed.
430       *
431       * @return  The response to the operation.
432       */
433      Response putUser(final RequestContext requestContext,
434                       final String userID,
435                       final InputStream inputStream)
436      {
437        final Unmarshaller unmarshaller;
438        if (requestContext.getConsumeMediaType().equals(
439            MediaType.APPLICATION_JSON_TYPE))
440        {
441          unmarshaller = new JsonUnmarshaller();
442          resourceStats.incrementStat(ResourceStats.PUT_CONTENT_JSON);
443        }
444        else
445        {
446          unmarshaller = new XmlUnmarshaller();
447          resourceStats.incrementStat(ResourceStats.PUT_CONTENT_XML);
448        }
449    
450        Response.ResponseBuilder responseBuilder;
451        try {
452          String authID = requestContext.getAuthID();
453          if(authID == null) {
454            throw new UnauthorizedException("Invalid credentials");
455          }
456    
457          // Parse the resource.
458          final BaseResource puttedResource = unmarshaller.unmarshal(
459              inputStream, resourceDescriptor, BaseResource.BASE_RESOURCE_FACTORY);
460    
461          final String attributes =
462              requestContext.getUriInfo().getQueryParameters().getFirst(
463                  QUERY_PARAMETER_ATTRIBUTES);
464          final SCIMQueryAttributes queryAttributes =
465              new SCIMQueryAttributes(resourceDescriptor, attributes);
466    
467          // Process the request.
468          final PutResourceRequest putResourceRequest =
469              new PutResourceRequest(requestContext.getUriInfo().getBaseUri(),
470                  authID, resourceDescriptor, userID,
471                  puttedResource.getScimObject(), queryAttributes);
472    
473    
474          final BaseResource scimResponse = backend.putResource(putResourceRequest);
475          // Build the response.
476          responseBuilder = Response.status(Response.Status.OK);
477          setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
478                            scimResponse);
479          responseBuilder.location(scimResponse.getMeta().getLocation());
480          resourceStats.incrementStat(ResourceStats.PUT_OK);
481        } catch (SCIMException e) {
482          // Build the response.
483          responseBuilder = Response.status(e.getStatusCode());
484          setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
485                            e);
486          resourceStats.incrementStat("put-" + e.getStatusCode());
487        }
488    
489        if(requestContext.getProduceMediaType() == MediaType.APPLICATION_JSON_TYPE)
490        {
491          resourceStats.incrementStat(ResourceStats.PUT_RESPONSE_JSON);
492        }
493        else if(requestContext.getProduceMediaType() ==
494                MediaType.APPLICATION_XML_TYPE)
495        {
496          resourceStats.incrementStat(ResourceStats.PUT_RESPONSE_XML);
497        }
498    
499        return responseBuilder.build();
500      }
501    
502    
503    
504      /**
505       * Process a DELETE operation.
506       *
507       * @param requestContext    The request context.
508       * @param userID     The target user ID.
509       *
510       * @return  The response to the operation.
511       */
512      Response deleteUser(final RequestContext requestContext,
513                          final String userID)
514      {
515        // Process the request.
516        Response.ResponseBuilder responseBuilder;
517        try {
518          final String authID = requestContext.getAuthID();
519          if(authID == null) {
520            throw new UnauthorizedException("Invalid credentials");
521          }
522          final DeleteResourceRequest deleteResourceRequest =
523            new DeleteResourceRequest(requestContext.getUriInfo().getBaseUri(),
524                authID, resourceDescriptor, userID);
525          backend.deleteResource(deleteResourceRequest);
526          // Build the response.
527          responseBuilder = Response.status(Response.Status.OK);
528          resourceStats.incrementStat(ResourceStats.DELETE_OK);
529        } catch (SCIMException e) {
530          // Build the response.
531          responseBuilder = Response.status(e.getStatusCode());
532          setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
533                            e);
534          resourceStats.incrementStat("delete-" + e.getStatusCode());
535        }
536    
537        return responseBuilder.build();
538      }
539    
540    
541    
542      /**
543       * Sets the response entity (content) for a SCIM response.
544       *
545       * @param builder       A JAX-RS response builder.
546       * @param mediaType     The media type to be returned.
547       * @param scimResponse  The SCIM response to be returned.
548       */
549      private void setResponseEntity(final Response.ResponseBuilder builder,
550                                     final MediaType mediaType,
551                                     final SCIMResponse scimResponse)
552      {
553        final Marshaller marshaller;
554        builder.type(mediaType);
555        if (mediaType.equals(MediaType.APPLICATION_JSON_TYPE))
556        {
557          marshaller = new JsonMarshaller();
558        }
559        else
560        {
561          marshaller = new XmlMarshaller();
562        }
563    
564        final StreamingOutput output = new StreamingOutput()
565        {
566          public void write(final OutputStream outputStream)
567              throws IOException, WebApplicationException
568          {
569            try
570            {
571              scimResponse.marshal(marshaller, outputStream);
572            }
573            catch (Exception e)
574            {
575              Debug.debugException(e);
576              throw new WebApplicationException(
577                  e, Response.Status.INTERNAL_SERVER_ERROR);
578            }
579          }
580        };
581        builder.entity(output);
582      }
583    }