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