001/*
002 * Copyright 2012-2016 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.BulkConfig;
021import com.unboundid.scim.data.ServiceProviderConfig;
022import com.unboundid.scim.marshal.Marshaller;
023import com.unboundid.scim.marshal.Unmarshaller;
024import com.unboundid.scim.marshal.json.JsonMarshaller;
025import com.unboundid.scim.marshal.json.JsonUnmarshaller;
026import com.unboundid.scim.marshal.xml.XmlMarshaller;
027import com.unboundid.scim.marshal.xml.XmlUnmarshaller;
028import com.unboundid.scim.sdk.BulkStreamResponse;
029import com.unboundid.scim.sdk.Debug;
030import com.unboundid.scim.sdk.DebugType;
031import com.unboundid.scim.sdk.OAuthTokenHandler;
032import com.unboundid.scim.sdk.SCIMException;
033import com.unboundid.scim.sdk.SCIMResponse;
034import com.unboundid.scim.sdk.ServerErrorException;
035import com.unboundid.scim.sdk.UnauthorizedException;
036
037import javax.ws.rs.WebApplicationException;
038import javax.ws.rs.core.MediaType;
039import javax.ws.rs.core.Response;
040import javax.ws.rs.core.StreamingOutput;
041import java.io.File;
042import java.io.FileOutputStream;
043import java.io.IOException;
044import java.io.InputStream;
045import java.io.OutputStream;
046import java.util.logging.Level;
047
048
049
050/**
051 * This class is the base class for JAX-RS resources implementing the Bulk
052 * operation.
053 */
054public class AbstractBulkResource
055{
056  private static final String RESOURCE_NAME = "Bulk";
057
058  /**
059   * The SCIM JAX-RS application associated with this resource.
060   */
061  private final SCIMApplication application;
062
063  /**
064   * The OAuth 2.0 bearer token handler. This may be null.
065   */
066  private final OAuthTokenHandler tokenHandler;
067
068  /**
069   * Create a new instance of the bulk resource.
070   *
071   * @param application        The SCIM JAX-RS application associated with this
072   *                           resource.
073   * @param tokenHandler       The token handler to use for OAuth
074   *                           authentication.
075   */
076  public AbstractBulkResource(final SCIMApplication application,
077                              final OAuthTokenHandler tokenHandler)
078  {
079    this.application       = application;
080    this.tokenHandler      = tokenHandler;
081  }
082
083
084
085  /**
086   * Process a POST operation.
087   *
088   * @param requestContext    The request context.
089   * @param inputStream       The content to be consumed.
090   *
091   * @return  The response to the operation.
092   */
093  Response postBulk(final RequestContext requestContext,
094                    final InputStream inputStream)
095  {
096    final Unmarshaller unmarshaller;
097    if (requestContext.getConsumeMediaType().equals(
098        MediaType.APPLICATION_JSON_TYPE))
099    {
100      unmarshaller = new JsonUnmarshaller();
101      application.getStatsForResource(RESOURCE_NAME).incrementStat(
102          ResourceStats.POST_CONTENT_JSON);
103    }
104    else
105    {
106      unmarshaller = new XmlUnmarshaller();
107      application.getStatsForResource(RESOURCE_NAME).incrementStat(
108          ResourceStats.POST_CONTENT_XML);
109    }
110
111    Response.ResponseBuilder responseBuilder;
112    try
113    {
114      String authID = requestContext.getAuthID();
115      if(authID == null && tokenHandler == null)
116      {
117        throw new UnauthorizedException("Invalid credentials");
118      }
119
120      // Check the Content-Length against the maxPayloadSize.
121      final ServiceProviderConfig serviceProviderConfig =
122          application.getServiceProviderConfig();
123      final BulkConfig bulkConfig = serviceProviderConfig.getBulkConfig();
124      if (requestContext.getContentLength() > bulkConfig.getMaxPayloadSize())
125      {
126        throw SCIMException.createException(
127            413, "The content length of the bulk request (" +
128                 requestContext.getContentLength() +
129                 ") exceeds the maxPayloadSize (" +
130                 bulkConfig.getMaxPayloadSize() + ")");
131      }
132
133      // Fail the request if the maximum concurrent requests would be exceeded.
134      application.acquireBulkRequestPermit();
135      try
136      {
137        // Write the request to a temporary file.
138        final File requestFile = File.createTempFile(
139            "scim-bulk-request-",
140            "." + requestContext.getConsumeMediaType().getSubtype(),
141            application.getTmpDataDir());
142        try
143        {
144          requestFile.deleteOnExit();
145          final FileOutputStream fileOutputStream =
146              new FileOutputStream(requestFile);
147          try
148          {
149            final byte[] buffer = new byte[8192];
150            int bytesRead;
151            long totalBytes = 0;
152            while ((bytesRead = inputStream.read(buffer)) != -1)
153            {
154              totalBytes += bytesRead;
155              if (totalBytes > bulkConfig.getMaxPayloadSize())
156              {
157                throw SCIMException.createException(
158                    413,
159                    "The size of the bulk request exceeds the maxPayloadSize " +
160                    "(" + bulkConfig.getMaxPayloadSize() + ")");
161              }
162              fileOutputStream.write(buffer, 0, bytesRead);
163            }
164          }
165          finally
166          {
167            fileOutputStream.close();
168          }
169
170          // Write the response to a temporary file.
171          final BulkStreamResponse bulkStreamResponse =
172              new BulkStreamResponse(application, requestContext);
173          try
174          {
175            final BulkContentRequestHandler handler =
176                new BulkContentRequestHandler(application, requestContext,
177                                              application.getBackend(),
178                                              bulkStreamResponse,
179                                              tokenHandler);
180            unmarshaller.bulkUnmarshal(requestFile, bulkConfig, handler);
181
182            // Build the response.
183            responseBuilder = Response.status(Response.Status.OK);
184            setResponseEntity(responseBuilder,
185                              requestContext.getProduceMediaType(),
186                              bulkStreamResponse);
187            application.getStatsForResource(RESOURCE_NAME).incrementStat(
188                ResourceStats.POST_OK);
189          }
190          catch (Exception e)
191          {
192            Debug.debugException(e);
193            bulkStreamResponse.finalizeResponse();
194            throw e;
195          }
196        }
197        finally
198        {
199          if (!requestFile.delete())
200          {
201            Debug.debug(Level.WARNING, DebugType.OTHER,
202                        "Could not delete temporary file " +
203                        requestFile.getAbsolutePath());
204          }
205        }
206      }
207      catch (SCIMException e)
208      {
209        throw e;
210      }
211      catch (Exception e)
212      {
213        Debug.debugException(e);
214        throw new ServerErrorException(
215            "Error processing bulk request: " + e.getMessage());
216      }
217      finally
218      {
219        application.releaseBulkRequestPermit();
220      }
221    }
222    catch (SCIMException e)
223    {
224      Debug.debugException(e);
225      // Build the response.
226      responseBuilder = Response.status(e.getStatusCode());
227      setResponseEntity(responseBuilder, requestContext.getProduceMediaType(),
228                        e);
229      application.getStatsForResource(RESOURCE_NAME).incrementStat(
230          "post-" + e.getStatusCode());
231    }
232
233    if (requestContext.getProduceMediaType() == MediaType.APPLICATION_JSON_TYPE)
234    {
235      application.getStatsForResource(RESOURCE_NAME).incrementStat(
236          ResourceStats.POST_RESPONSE_JSON);
237    }
238    else if (requestContext.getProduceMediaType() ==
239             MediaType.APPLICATION_XML_TYPE)
240    {
241      application.getStatsForResource(RESOURCE_NAME).incrementStat(
242          ResourceStats.POST_RESPONSE_XML);
243    }
244
245    return responseBuilder.build();
246  }
247
248
249
250  /**
251   * Sets the response entity (content) for a SCIM bulk response.
252   *
253   * @param builder       A JAX-RS response builder.
254   * @param mediaType     The media type to be returned.
255   * @param scimResponse  The SCIM response to be returned.
256   */
257  private void setResponseEntity(final Response.ResponseBuilder builder,
258                                 final MediaType mediaType,
259                                 final SCIMResponse scimResponse)
260  {
261    final Marshaller marshaller;
262    builder.type(mediaType);
263    if (mediaType.equals(MediaType.APPLICATION_JSON_TYPE))
264    {
265      marshaller = new JsonMarshaller();
266    }
267    else
268    {
269      marshaller = new XmlMarshaller();
270    }
271
272    final StreamingOutput output = new StreamingOutput()
273    {
274      public void write(final OutputStream outputStream)
275          throws IOException, WebApplicationException
276      {
277        try
278        {
279          scimResponse.marshal(marshaller, outputStream);
280        }
281        catch (Exception e)
282        {
283          Debug.debugException(e);
284          throw new WebApplicationException(
285              e, Response.Status.INTERNAL_SERVER_ERROR);
286        }
287      }
288    };
289    builder.entity(output);
290  }
291}