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    }