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.marshal.json;
019    
020    import com.unboundid.scim.data.BaseResource;
021    import com.unboundid.scim.data.BulkConfig;
022    import com.unboundid.scim.marshal.BulkInputStreamWrapper;
023    import com.unboundid.scim.schema.AttributeDescriptor;
024    import com.unboundid.scim.schema.ResourceDescriptor;
025    import com.unboundid.scim.sdk.BulkContentHandler;
026    import com.unboundid.scim.sdk.BulkOperation;
027    import com.unboundid.scim.sdk.Debug;
028    import com.unboundid.scim.sdk.InvalidResourceException;
029    import com.unboundid.scim.sdk.SCIMAttribute;
030    import com.unboundid.scim.sdk.SCIMAttributeValue;
031    import com.unboundid.scim.sdk.SCIMException;
032    import com.unboundid.scim.sdk.Status;
033    import org.json.JSONException;
034    import org.json.JSONObject;
035    import org.json.JSONTokener;
036    
037    import java.io.InputStream;
038    import java.util.Arrays;
039    
040    
041    
042    /**
043     * This class is a helper class to handle parsing of JSON bulk operations.
044     */
045    public class JsonBulkParser extends JsonParser
046    {
047      private final BulkInputStreamWrapper bulkInputStream;
048      private final BulkConfig bulkConfig;
049      private final BulkContentHandler handler;
050      private int operationIndex = 0;
051      private JSONTokener tokener;
052      private boolean skipOperations;
053    
054      /**
055       * Create a new instance of this bulk unmarshaller.
056       *
057       * @param inputStream  The input stream containing the bulk content to be
058       *                     read.
059       * @param bulkConfig   The bulk configuration settings to be enforced.
060       * @param handler      A bulk operation listener to handle the content as it
061       *                     is read.
062       */
063      public JsonBulkParser(final InputStream inputStream,
064                            final BulkConfig bulkConfig,
065                            final BulkContentHandler handler)
066      {
067        this.bulkInputStream = new BulkInputStreamWrapper(inputStream);
068        this.bulkConfig      = bulkConfig;
069        this.handler         = handler;
070        this.operationIndex = 0;
071      }
072    
073    
074    
075      /**
076       * Specify whether bulk operations should be skipped.
077       *
078       * @param skipOperations  {@code true} if bulk operations should be skipped.
079       */
080      public void setSkipOperations(final boolean skipOperations)
081      {
082        this.skipOperations = skipOperations;
083      }
084    
085    
086    
087      /**
088       * Reads a SCIM bulk request or response from the input stream.
089       *
090       * @throws SCIMException If the bulk content could not be read.
091       */
092      public void unmarshal()
093          throws SCIMException
094      {
095        try
096        {
097          tokener = new JSONTokener(bulkInputStream);
098    
099          if (tokener.nextClean() != '{')
100          {
101              throw tokener.syntaxError("A JSONObject text must begin with '{'");
102          }
103          for (;;)
104          {
105            String key;
106            char c = tokener.nextClean();
107            switch (c)
108            {
109              case 0:
110                throw tokener.syntaxError("A JSONObject text must end with '}'");
111              case '}':
112                return;
113              default:
114                tokener.back();
115                key = tokener.nextValue().toString();
116            }
117    
118            // The key is followed by ':'. We will also tolerate '=' or '=>'.
119    
120            c = tokener.nextClean();
121            if (c == '=')
122            {
123              if (tokener.next() != '>')
124              {
125                tokener.back();
126              }
127            } else if (c != ':')
128            {
129              throw tokener.syntaxError("Expected a ':' after a key");
130            }
131    
132            if (key.equals("failOnErrors"))
133            {
134              handler.handleFailOnErrors((Integer)tokener.nextValue());
135            }
136            else if (key.equals("Operations"))
137            {
138              parseOperations();
139            }
140            else
141            {
142              // Skip.
143              tokener.nextValue();
144            }
145    
146            // Pairs are separated by ','. We will also tolerate ';'.
147    
148            switch (tokener.nextClean())
149            {
150              case ';':
151              case ',':
152                if (tokener.nextClean() == '}')
153                {
154                  return;
155                }
156                tokener.back();
157                break;
158              case '}':
159                return;
160              default:
161                throw tokener.syntaxError("Expected a ',' or '}'");
162            }
163          }
164        }
165        catch (SCIMException e)
166        {
167          throw e;
168        }
169        catch (Exception e)
170        {
171          Debug.debugException(e);
172          throw new InvalidResourceException(
173              "Error while reading JSON Bulk content: " + e.getMessage(), e);
174        }
175      }
176    
177    
178    
179      /**
180       * Parse the Operations element.
181       *
182       * @throws JSONException  If the JSON could not be parsed.
183       * @throws SCIMException  If some other error occurred.
184       */
185      private void parseOperations()
186          throws JSONException, SCIMException
187      {
188        if (tokener.nextClean() != '[')
189        {
190            throw this.tokener.syntaxError("A JSONArray text must start with '['");
191        }
192        if (tokener.nextClean() != ']')
193        {
194          tokener.back();
195          for (;;)
196          {
197            if (tokener.nextClean() != ',')
198            {
199              tokener.back();
200              if (operationIndex >= bulkConfig.getMaxOperations())
201              {
202                throw SCIMException.createException(
203                    413,
204                    "The number of operations in the bulk operation exceeds " +
205                    "maxOperations (" + bulkConfig.getMaxOperations() + ")");
206              }
207    
208              if (bulkInputStream.getBytesRead() > bulkConfig.getMaxPayloadSize())
209              {
210                throw SCIMException.createException(
211                    413,
212                    "The size of the bulk operation exceeds the maxPayloadSize " +
213                    "(" + bulkConfig.getMaxPayloadSize() + ")");
214              }
215              if (skipOperations)
216              {
217                tokener.nextValue();
218              }
219              else
220              {
221                final BulkOperation bulkOperation =
222                    parseBulkOperation((JSONObject)tokener.nextValue());
223                handler.handleOperation(operationIndex, bulkOperation);
224              }
225              operationIndex++;
226            }
227    
228            switch (tokener.nextClean())
229            {
230              case ';':
231              case ',':
232                if (tokener.nextClean() == ']')
233                {
234                  return;
235                }
236                tokener.back();
237                break;
238              case ']':
239                return;
240              default:
241                throw tokener.syntaxError("Expected a ',' or ']'");
242            }
243          }
244        }
245      }
246    
247    
248    
249      /**
250       * Parse an individual operation in a bulk operation request or response.
251       *
252       * @param o            The JSON object representing the operation.
253       *
254       * @return  The parsed bulk operation.
255       *
256       * @throws JSONException  If the operation cannot be parsed due to a JSON
257       *                        error.
258       * @throws SCIMException  If the operation cannot be parsed for some other
259       *                        reason.
260       */
261      private BulkOperation parseBulkOperation(
262          final JSONObject o)
263      throws JSONException, SCIMException
264      {
265        final String httpMethod = o.getString("method");
266        final String bulkId = o.optString("bulkId", null);
267        final String version = o.optString("version", null);
268        final String path = o.optString("path", null);
269        final String location = o.optString("location", null);
270        final JSONObject data = o.optJSONObject("data");
271        final JSONObject statusObject = o.optJSONObject("status");
272    
273        final Status status;
274        if (statusObject != null)
275        {
276          final String code = statusObject.getString("code");
277          final String description = statusObject.optString("description", null);
278          status = new Status(code, description);
279        }
280        else
281        {
282          status = null;
283        }
284    
285        final BulkOperation.Method method;
286        try
287        {
288          method = BulkOperation.Method.valueOf(httpMethod);
289        }
290        catch (IllegalArgumentException e)
291        {
292          throw SCIMException.createException(
293              405, "Bulk operation " + operationIndex + " specifies an invalid " +
294                   "HTTP method '" + httpMethod + "'. Allowed methods are " +
295                   Arrays.asList(BulkOperation.Method.values()));
296        }
297    
298        String endpoint = null;
299        if (path != null)
300        {
301          int startPos = 0;
302          if (path.charAt(startPos) == '/')
303          {
304            startPos++;
305          }
306    
307          int endPos = path.indexOf('/', startPos);
308          if (endPos == -1)
309          {
310            endPos = path.length();
311          }
312    
313          endpoint = path.substring(startPos, endPos);
314        }
315    
316        BaseResource resource = null;
317        if (data != null)
318        {
319          if (path == null)
320          {
321            throw new InvalidResourceException(
322                "Bulk operation " + operationIndex + " has data but no path");
323          }
324    
325          final ResourceDescriptor descriptor =
326              handler.getResourceDescriptor(endpoint);
327          if (descriptor == null)
328          {
329            throw new InvalidResourceException(
330                "Bulk operation " + operationIndex + " specifies an unknown " +
331                "resource endpoint '" + endpoint + "'");
332          }
333    
334          resource = unmarshal(data, descriptor,
335                               BaseResource.BASE_RESOURCE_FACTORY, null);
336        }
337    
338        return new BulkOperation(method, bulkId, version, path, location, resource,
339                                 status);
340      }
341    
342    
343    
344      /**
345       * Parse a simple attribute from its representation as a JSON Object.
346       *
347       * @param jsonAttribute       The JSON object representing the attribute.
348       * @param attributeDescriptor The attribute descriptor.
349       *
350       * @return The parsed attribute.
351       */
352      protected SCIMAttribute createSimpleAttribute(
353          final Object jsonAttribute,
354          final AttributeDescriptor attributeDescriptor)
355      {
356        final String v =
357            handler.transformValue(operationIndex, jsonAttribute.toString());
358    
359        return SCIMAttribute.create(
360            attributeDescriptor,
361            SCIMAttributeValue.createValue(attributeDescriptor.getDataType(), v));
362      }
363    }