001 /*
002 * Copyright 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.schema.ResourceDescriptor;
022 import com.unboundid.scim.sdk.BulkContentHandler;
023 import com.unboundid.scim.sdk.BulkOperation;
024 import com.unboundid.scim.sdk.BulkStreamResponse;
025 import com.unboundid.scim.sdk.Debug;
026 import com.unboundid.scim.sdk.DeleteResourceRequest;
027 import com.unboundid.scim.sdk.InvalidResourceException;
028 import com.unboundid.scim.sdk.PostResourceRequest;
029 import com.unboundid.scim.sdk.PutResourceRequest;
030 import com.unboundid.scim.sdk.SCIMAttribute;
031 import com.unboundid.scim.sdk.SCIMAttributeValue;
032 import com.unboundid.scim.sdk.SCIMBackend;
033 import com.unboundid.scim.sdk.SCIMException;
034 import com.unboundid.scim.sdk.SCIMObject;
035 import com.unboundid.scim.sdk.SCIMQueryAttributes;
036 import com.unboundid.scim.sdk.ServerErrorException;
037 import com.unboundid.scim.sdk.Status;
038
039 import javax.ws.rs.core.MediaType;
040 import javax.ws.rs.core.UriBuilder;
041 import java.util.ArrayList;
042 import java.util.Collection;
043 import java.util.HashMap;
044 import java.util.HashSet;
045 import java.util.Map;
046 import java.util.Set;
047
048
049
050 /**
051 * This class implements the bulk operation handler to process bulk operation
052 * requests in the SCIM server.
053 * The original purpose of the BulkContentHandler interface was to allow
054 * each operation to be processed as soon as it had been read so that
055 * we do not have to hold the entire content of a bulk request in
056 * memory. However, there are two issues making that approach infeasible:
057 * <ol>
058 * <li>Since JSON objects are unordered, the failOnErrors value could
059 * conceivably come after the Operations array, which would be too late.</li>
060 * <li>It would not be possible to reject a request that exceeded the
061 * maxOperations setting without processing any operations.</li>
062 * </ol>
063 */
064 public class BulkContentRequestHandler extends BulkContentHandler
065 {
066 /**
067 * The resource descriptors keyed by endpoint.
068 */
069 private final Map<String,ResourceDescriptor> descriptors;
070
071 /**
072 * The SCIM application.
073 */
074 private final SCIMApplication application;
075
076 /**
077 * The request context for the bulk request.
078 */
079 private final RequestContext requestContext;
080
081 /**
082 * The SCIM backend to process the operations.
083 */
084 private final SCIMBackend backend;
085
086 /**
087 * The bulk stream response to write the operation responses to.
088 */
089 private final BulkStreamResponse bulkStreamResponse;
090
091 /**
092 * A map from bulkId to resourceID.
093 */
094 private final Map<String,String> resourceIDs;
095
096 /**
097 * A set containing the unresolved bulkId data references for the latest
098 * bulk operation.
099 */
100 private final Set<String> unresolvedBulkIdRefs;
101
102
103 private int errorCount = 0;
104
105 /**
106 * The set of defined bulkIds from all operations.
107 */
108 private final Set<String> bulkIds;
109
110
111
112 /**
113 * The number of errors that the Service Provider will accept before the
114 * operation is terminated and an error response is returned. The default
115 * is to continue performing as many changes as possible without regard to
116 * failures.
117 */
118 private int failOnErrors = Integer.MAX_VALUE;
119
120
121
122 /**
123 * Create a new instance of this bulk operation handler.
124 *
125 * @param application The SCIM application.
126 * @param requestContext The request context for the bulk request.
127 * @param backend The SCIM backend to process the operations.
128 * @param bulkStreamResponse The bulk stream response to write response
129 * operations to.
130 */
131 public BulkContentRequestHandler(
132 final SCIMApplication application,
133 final RequestContext requestContext,
134 final SCIMBackend backend,
135 final BulkStreamResponse bulkStreamResponse)
136 {
137 this.descriptors = application.getDescriptors();
138 this.application = application;
139 this.requestContext = requestContext;
140 this.backend = backend;
141 this.bulkStreamResponse = bulkStreamResponse;
142
143 resourceIDs = new HashMap<String, String>();
144 unresolvedBulkIdRefs = new HashSet<String>();
145 bulkIds = new HashSet<String>();
146 }
147
148
149
150 /**
151 * {@inheritDoc}
152 */
153 public void handleFailOnErrors(final int failOnErrors)
154 {
155 this.failOnErrors = failOnErrors;
156 }
157
158
159
160 /**
161 * {@inheritDoc}
162 */
163 @Override
164 public String transformValue(final int opIndex, final String value)
165 {
166 if (value.startsWith("bulkId:"))
167 {
168 final String bulkId = value.substring(7);
169 final String resourceID = resourceIDs.get(bulkId);
170 if (resourceID != null)
171 {
172 return resourceID;
173 }
174 else
175 {
176 unresolvedBulkIdRefs.add(bulkId);
177 }
178 }
179
180 return value;
181 }
182
183
184
185 /**
186 * {@inheritDoc}
187 */
188 @Override
189 public ResourceDescriptor getResourceDescriptor(final String endpoint)
190 {
191 return descriptors.get(endpoint);
192 }
193
194
195
196 /**
197 * {@inheritDoc}
198 */
199 public boolean handleOperation(final int opIndex,
200 final BulkOperation bulkOperation)
201 throws SCIMException
202 {
203 if (errorCount < failOnErrors)
204 {
205 final BulkOperation response = processOperation(opIndex, bulkOperation);
206 unresolvedBulkIdRefs.clear();
207 bulkStreamResponse.writeBulkOperation(response);
208 if (response.getStatus().getDescription() != null &&
209 !response.getStatus().getCode().equals("200") &&
210 !response.getStatus().getCode().equals("201"))
211 {
212 errorCount++;
213 if (errorCount == failOnErrors)
214 {
215 return false;
216 }
217 }
218
219 return true;
220 }
221 else
222 {
223 return false;
224 }
225 }
226
227
228
229 /**
230 * Process an operation from a bulk request.
231 *
232 * @param opIndex The index of the operation.
233 * @param operation The operation to be processed from the bulk request.
234 *
235 * @return The operation response.
236 */
237 private BulkOperation processOperation(final int opIndex,
238 final BulkOperation operation)
239 {
240 final BulkOperation.Method method = operation.getMethod();
241 final String bulkId = operation.getBulkId();
242 final String path = operation.getPath();
243 BaseResource resource = operation.getData();
244
245 int statusCode = 200;
246 String statusMessage = null;
247 String location = null;
248 String endpoint = null;
249 String resourceID = null;
250
251 final ResourceDescriptor descriptor;
252 final ResourceStats resourceStats;
253 try
254 {
255 if (method == null)
256 {
257 throw new InvalidResourceException(
258 "The bulk operation does not specify a HTTP method");
259 }
260
261 if (method == BulkOperation.Method.PATCH)
262 {
263 throw SCIMException.createException(501, "PATCH is not supported");
264 }
265
266 if (path == null)
267 {
268 throw new InvalidResourceException(
269 "The bulk operation does not specify a path");
270 }
271
272 if (path != null)
273 {
274 // Parse the path into an endpoint and optional resource ID.
275 int startPos = 0;
276 if (path.charAt(startPos) == '/')
277 {
278 startPos++;
279 }
280
281 int endPos = path.indexOf('/', startPos);
282 if (endPos == -1)
283 {
284 endPos = path.length();
285 }
286
287 endpoint = path.substring(startPos, endPos);
288
289 if (endPos < path.length() - 1)
290 {
291 resourceID = path.substring(endPos+1);
292 }
293
294 if (method == BulkOperation.Method.POST)
295 {
296 if (resourceID != null)
297 {
298 throw new InvalidResourceException(
299 "The bulk operation has method POST but the path includes" +
300 "a resource ID");
301 }
302 }
303 else
304 {
305 if (resourceID == null)
306 {
307 throw new InvalidResourceException(
308 "The bulk operation does not have a resource ID in " +
309 "the path");
310 }
311
312 if (resourceID.startsWith("bulkId:"))
313 {
314 final String ref = resourceID.substring(7);
315 resourceID = resourceIDs.get(ref);
316 if (resourceID == null)
317 {
318 throw SCIMException.createException(
319 409, "Cannot resolve bulkId reference '" + ref + "'");
320 }
321 }
322 }
323 }
324
325 descriptor = getResourceDescriptor(endpoint);
326 if (descriptor == null)
327 {
328 throw new InvalidResourceException(
329 "The bulk operation specifies an unknown resource " +
330 "endpoint '" + endpoint + "'");
331 }
332
333 resourceStats = application.getStatsForResource(descriptor.getName());
334 if (resourceStats == null)
335 {
336 throw new ServerErrorException(
337 "Cannot find resource stats for resource '" +
338 descriptor.getName() + "'");
339 }
340 }
341 catch (SCIMException e)
342 {
343 Debug.debugException(e);
344 statusCode = e.getStatusCode();
345 statusMessage = e.getMessage();
346
347 final Status status =
348 new Status(String.valueOf(statusCode), statusMessage);
349
350 return new BulkOperation(method, bulkId, null, null, location,
351 null, status);
352 }
353
354 try
355 {
356 final UriBuilder locationBuilder =
357 UriBuilder.fromUri(requestContext.getUriInfo().getBaseUri());
358 locationBuilder.path(path);
359
360 if (method == BulkOperation.Method.POST && bulkId == null)
361 {
362 throw new InvalidResourceException(
363 "The bulk operation has method POST but does not " +
364 "specify a bulkId");
365 }
366
367 if (bulkId != null)
368 {
369 if (!bulkIds.add(bulkId))
370 {
371 throw new InvalidResourceException(
372 "The bulk operation defines a duplicate bulkId '" +
373 bulkId + "'");
374 }
375 }
376
377 if (method != BulkOperation.Method.DELETE && resource == null)
378 {
379 throw new InvalidResourceException(
380 "The bulk operation does not have any resource data");
381 }
382
383 if (!unresolvedBulkIdRefs.isEmpty())
384 {
385 throw SCIMException.createException(
386 409, "Cannot resolve bulkId references "
387 + unresolvedBulkIdRefs);
388 }
389
390 if (requestContext.getConsumeMediaType().equals(
391 MediaType.APPLICATION_JSON_TYPE))
392 {
393 switch (method)
394 {
395 case POST:
396 resourceStats.incrementStat(ResourceStats.POST_CONTENT_JSON);
397 break;
398 case PUT:
399 resourceStats.incrementStat(ResourceStats.PUT_CONTENT_JSON);
400 break;
401 }
402 }
403 else
404 {
405 switch (method)
406 {
407 case POST:
408 resourceStats.incrementStat(ResourceStats.POST_CONTENT_XML);
409 break;
410 case PUT:
411 resourceStats.incrementStat(ResourceStats.PUT_CONTENT_XML);
412 break;
413 }
414 }
415
416 // Request no attributes because we will not provide the resource in
417 // the response.
418 final SCIMQueryAttributes queryAttributes =
419 new SCIMQueryAttributes(descriptor, "");
420
421 switch (method)
422 {
423 case POST:
424 final BaseResource postedResource = backend.postResource(
425 new PostResourceRequest(requestContext.getUriInfo().getBaseUri(),
426 requestContext.getAuthID(),
427 descriptor,
428 resource.getScimObject(),
429 queryAttributes));
430
431 resourceID = postedResource.getId();
432 locationBuilder.path(resourceID);
433 location = locationBuilder.build().toString();
434 statusCode = 201;
435 resourceStats.incrementStat(ResourceStats.POST_OK);
436 break;
437
438 case PUT:
439 backend.putResource(
440 new PutResourceRequest(requestContext.getUriInfo().getBaseUri(),
441 requestContext.getAuthID(),
442 descriptor,
443 resourceID,
444 resource.getScimObject(),
445 queryAttributes));
446 location = locationBuilder.build().toString();
447 resourceStats.incrementStat(ResourceStats.PUT_OK);
448 break;
449
450 case DELETE:
451 backend.deleteResource(new DeleteResourceRequest(
452 requestContext.getUriInfo().getBaseUri(),
453 requestContext.getAuthID(),
454 descriptor,
455 resourceID));
456 location = locationBuilder.build().toString();
457 resourceStats.incrementStat(ResourceStats.DELETE_OK);
458 break;
459 }
460
461 if (bulkId != null)
462 {
463 resourceIDs.put(bulkId, resourceID);
464 }
465 }
466 catch (SCIMException e)
467 {
468 Debug.debugException(e);
469 statusCode = e.getStatusCode();
470 statusMessage = e.getMessage();
471 switch (method)
472 {
473 case POST:
474 resourceStats.incrementStat("post-" + e.getStatusCode());
475 break;
476 case PUT:
477 resourceStats.incrementStat("put-" + e.getStatusCode());
478 break;
479 case DELETE:
480 resourceStats.incrementStat("delete-" + e.getStatusCode());
481 break;
482 }
483 }
484
485 if (requestContext.getProduceMediaType() ==
486 MediaType.APPLICATION_JSON_TYPE)
487 {
488 switch (method)
489 {
490 case POST:
491 resourceStats.incrementStat(ResourceStats.POST_RESPONSE_JSON);
492 break;
493 case PUT:
494 resourceStats.incrementStat(ResourceStats.PUT_RESPONSE_JSON);
495 break;
496 }
497 }
498 else if (requestContext.getProduceMediaType() ==
499 MediaType.APPLICATION_XML_TYPE)
500 {
501 switch (method)
502 {
503 case POST:
504 resourceStats.incrementStat(ResourceStats.POST_RESPONSE_XML);
505 break;
506 case PUT:
507 resourceStats.incrementStat(ResourceStats.PUT_RESPONSE_XML);
508 break;
509 }
510 }
511
512 final Status status =
513 new Status(String.valueOf(statusCode), statusMessage);
514
515 return new BulkOperation(method, bulkId, null, null, location,
516 null, status);
517 }
518
519
520
521 /**
522 * Obtain a copy of the provided resource with each bulkId reference
523 * resolved to a resource ID.
524 *
525 * @param resource The resource that may contain bulkId references.
526 *
527 * @return A copy of the resource with bulkId references resolved.
528 *
529 * @throws SCIMException If there are any undefined bulkId references.
530 */
531 private BaseResource resolveBulkIds(final BaseResource resource)
532 throws SCIMException
533 {
534 final SCIMObject src = resource.getScimObject();
535 final SCIMObject dst = new SCIMObject();
536
537 for (final String schema : src.getSchemas())
538 {
539 for (final SCIMAttribute a : src.getAttributes(schema))
540 {
541 dst.setAttribute(resolveBulkIds(a));
542 }
543 }
544
545 return new BaseResource(resource.getResourceDescriptor(), dst);
546 }
547
548
549
550 /**
551 * Obtain a copy of the provided SCIM attribute with each bulkId reference
552 * resolved to a resource ID.
553 *
554 * @param a The attribute that may contain bulkId references.
555 *
556 * @return A copy of the attribute with bulkId references resolved.
557 *
558 * @throws SCIMException If there are any undefined bulkId references.
559 */
560 private SCIMAttribute resolveBulkIds(final SCIMAttribute a)
561 throws SCIMException
562 {
563 final SCIMAttributeValue[] srcValues = a.getValues();
564 final SCIMAttributeValue[] dstValues =
565 new SCIMAttributeValue[a.getValues().length];
566 for (int i = 0; i < srcValues.length; i++)
567 {
568 dstValues[i] = resolveBulkIds(srcValues[i]);
569 }
570
571 return SCIMAttribute.create(a.getAttributeDescriptor(), dstValues);
572 }
573
574
575
576 /**
577 * Obtain a copy of the provided SCIM attribute value with each bulkId
578 * reference resolved to a resource ID.
579 *
580 * @param v The attribute value that may contain bulkId references.
581 *
582 * @return A copy of the attribute value with bulkId references resolved.
583 *
584 * @throws SCIMException If there are any undefined bulkId references.
585 */
586 private SCIMAttributeValue resolveBulkIds(final SCIMAttributeValue v)
587 throws SCIMException
588 {
589 if (v.isComplex())
590 {
591 final Collection<SCIMAttribute> srcAttrs = v.getAttributes().values();
592 final ArrayList<SCIMAttribute> dstAttrs =
593 new ArrayList<SCIMAttribute>(srcAttrs.size());
594 for (final SCIMAttribute a : srcAttrs)
595 {
596 dstAttrs.add(resolveBulkIds(a));
597 }
598
599 return SCIMAttributeValue.createComplexValue(dstAttrs);
600 }
601 else
602 {
603 final String s = v.getStringValue();
604 if (s.startsWith("bulkId:"))
605 {
606 final String bulkId = s.substring(7);
607 final String resourceID = resourceIDs.get(bulkId);
608 if (resourceID != null)
609 {
610 return SCIMAttributeValue.createStringValue(resourceID);
611 }
612 else
613 {
614 throw SCIMException.createException(
615 409, "Cannot resolve bulkId reference '" + bulkId + "'");
616 }
617 }
618 else
619 {
620 return v;
621 }
622 }
623 }
624 }