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