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 }