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.ResourceFactory;
022import com.unboundid.scim.schema.AttributeDescriptor;
023import com.unboundid.scim.schema.CoreSchema;
024import com.unboundid.scim.schema.ResourceDescriptor;
025import com.unboundid.scim.sdk.InvalidResourceException;
026import com.unboundid.scim.sdk.SCIMAttribute;
027import com.unboundid.scim.sdk.SCIMAttributeValue;
028import com.unboundid.scim.sdk.SCIMConstants;
029import com.unboundid.scim.sdk.SCIMObject;
030import org.json.JSONArray;
031import org.json.JSONException;
032import org.json.JSONObject;
033
034import java.util.ArrayList;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.Iterator;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041
042import static com.unboundid.scim.sdk.StaticUtils.toLowerCase;
043
044
045
046/**
047 * Helper class for JSON unmarshalling.
048 */
049public class JsonParser
050{
051  /**
052   * Read a SCIM resource from the specified JSON object.
053   *
054   * @param <R> The type of resource instance.
055   * @param jsonObject  The JSON object to be read.
056   * @param resourceDescriptor The descriptor of the SCIM resource to be read.
057   * @param resourceFactory The resource factory to use to create the resource
058   *                        instance.
059   * @param defaultSchemas  The set of schemas used by attributes of the
060   *                        resource, or {@code null} if the schemas must be
061   *                        provided in the resource object.
062   *
063   * @return  The SCIM resource that was read.
064   *
065   * @throws JSONException If an error occurred.
066   * @throws InvalidResourceException if a schema error occurs.
067   */
068  protected <R extends BaseResource> R unmarshal(
069      final JSONObject jsonObject,
070      final ResourceDescriptor resourceDescriptor,
071      final ResourceFactory<R> resourceFactory,
072      final JSONArray defaultSchemas)
073      throws JSONException, InvalidResourceException
074  {
075    try
076    {
077      final SCIMObject scimObject = new SCIMObject();
078      final boolean implicitSchemaChecking = Boolean.getBoolean(
079              SCIMConstants.IMPLICIT_SCHEMA_CHECKING_PROPERTY);
080
081      // The first keyed object ought to be a schemas array, but it may not be
082      // present if 1) the attrs are all core and 2) the client decided to omit
083      // the schema declaration.
084      final JSONArray schemas;
085      if (jsonObject.has(SCIMConstants.SCHEMAS_ATTRIBUTE_NAME))
086      {
087        schemas = jsonObject.getJSONArray(SCIMConstants.SCHEMAS_ATTRIBUTE_NAME);
088      }
089      else if (defaultSchemas != null)
090      {
091        schemas = defaultSchemas;
092      }
093      else
094      {
095        String[] schemaArray = new String[1];
096        schemaArray[0] = resourceDescriptor.getSchema();
097        schemas = new JSONArray(schemaArray);
098      }
099
100      final Set<String> schemaSet = new HashSet<String>(schemas.length());
101      if (implicitSchemaChecking)
102      {
103        schemaSet.addAll(resourceDescriptor.getAttributeSchemas());
104      }
105      for (int i = 0; i < schemas.length(); i++)
106      {
107        schemaSet.add(toLowerCase(schemas.getString(i)));
108      }
109
110      final Iterator k = jsonObject.keys();
111      while (k.hasNext())
112      {
113        final String attributeKey = (String) k.next();
114        final String attributeKeyLower = toLowerCase(attributeKey);
115
116        if(SCIMConstants.SCHEMAS_ATTRIBUTE_NAME.equals(attributeKeyLower))
117        {
118          continue;
119        }
120
121        if (schemaSet.contains(attributeKeyLower))
122        {
123          //This key is a container for some extended schema
124          JSONObject schemaAttrs = jsonObject.getJSONObject(attributeKey);
125          final Iterator keys = schemaAttrs.keys();
126          while (keys.hasNext())
127          {
128            final String attributeName = (String) keys.next();
129            final AttributeDescriptor attributeDescriptor =
130                   resourceDescriptor.getAttribute(attributeKey, attributeName);
131            final Object jsonAttribute = schemaAttrs.get(attributeName);
132            final SCIMAttribute a = create(attributeDescriptor, jsonAttribute);
133            if (a != null)
134            {
135              scimObject.addAttribute(a);
136            }
137          }
138        }
139        else
140        {
141          final Object jsonAttribute = jsonObject.get(attributeKey);
142          if (implicitSchemaChecking)
143          {
144            //Try to determine the schema for this attribute
145            final String attributeName = attributeKey;
146            final String schema =
147                    resourceDescriptor.findAttributeSchema(attributeName);
148            final AttributeDescriptor attributeDescriptor =
149                    resourceDescriptor.getAttribute(schema, attributeName);
150            if (CoreSchema.META_DESCRIPTOR.equals(attributeDescriptor))
151            {
152              try
153              {
154                // Special implicit schema processing for meta.attributes
155                // which contains the names of the attributes to remove from
156                // the Resource during a PATCH operation.  These each should be
157                // fully qualified with schema urn by the client, but if they
158                // are not we can try to determine the schema here.
159                JSONObject jsonMetaObj = ((JSONObject)jsonAttribute);
160                JSONArray metaAttrs = null;
161                final Iterator keys = jsonMetaObj.keys();
162                while (keys.hasNext())
163                {
164                  final String key = (String) keys.next();
165                  if ("attributes".equals(key.toLowerCase()))
166                  {
167                    Object attrObj = jsonMetaObj.get(key);
168                    if (attrObj instanceof JSONArray)
169                    {
170                      metaAttrs = (JSONArray) attrObj;
171                    }
172                    break;
173                  }
174                }
175                if (metaAttrs != null)
176                {
177                  JSONArray newMetaAttrs = new JSONArray();
178                  for (int i=0; i < metaAttrs.length(); i++)
179                  {
180                    String metaAttr = (String) metaAttrs.get(i);
181                    String metaSchema = resourceDescriptor.findAttributeSchema(
182                            metaAttr);
183                    // The schema returned will be null if attribute value was
184                    // already fully qualified.
185                    if (metaSchema != null)
186                    {
187                      metaAttr = metaSchema +
188                              SCIMConstants.SEPARATOR_CHAR_QUALIFIED_ATTRIBUTE +
189                              metaAttr;
190                    }
191                    newMetaAttrs.put(metaAttr);
192                  }
193                  jsonMetaObj.put("attributes", newMetaAttrs);
194                }
195              }
196              catch (Exception ignore)
197              {
198                // Don't fail because of implicit schema checking
199              }
200            }
201            final SCIMAttribute a = create(attributeDescriptor, jsonAttribute);
202            if (a != null)
203            {
204              scimObject.addAttribute(a);
205            }
206          }
207          else
208          {
209            if (!schemaSet.contains(SCIMConstants.SCHEMA_URI_CORE))
210            {
211              throw new Exception("'" + SCIMConstants.SCHEMA_URI_CORE +
212                      "' must be declared in the schemas attribute.");
213            }
214            final AttributeDescriptor attributeDescriptor =
215                      resourceDescriptor.getAttribute(
216                              SCIMConstants.SCHEMA_URI_CORE,
217                              attributeKey);
218            final SCIMAttribute a = create(attributeDescriptor, jsonAttribute);
219            if (a != null)
220            {
221              scimObject.addAttribute(a);
222            }
223          }
224        }
225      }
226
227      return resourceFactory.createResource(resourceDescriptor, scimObject);
228    }
229    catch (Exception e)
230    {
231      throw new InvalidResourceException(
232          "Resource '" + resourceDescriptor.getName() + "' is malformed: " +
233          e.getMessage(), e);
234    }
235  }
236
237
238
239  /**
240   * Parse a simple attribute from its representation as a JSON Object.
241   *
242   * @param jsonAttribute       The JSON object representing the attribute.
243   * @param attributeDescriptor The attribute descriptor.
244   *
245   * @return The parsed attribute.
246   */
247  protected SCIMAttribute createSimpleAttribute(
248      final Object jsonAttribute,
249      final AttributeDescriptor attributeDescriptor)
250  {
251    return SCIMAttribute.create(attributeDescriptor,
252        SCIMAttributeValue.createValue(attributeDescriptor.getDataType(),
253                                       jsonAttribute.toString()));
254  }
255
256
257
258  /**
259   * Parse a multi-valued attribute from its representation as a JSON Array.
260   *
261   * @param jsonAttribute       The JSON array representing the attribute.
262   * @param attributeDescriptor The attribute descriptor.
263   *
264   * @return The parsed attribute, or {@code null} if there are no non-null
265   *         values in the array.
266   *
267   * @throws JSONException Thrown if error creating multi-valued attribute.
268   * @throws InvalidResourceException if a schema error occurs.
269   */
270  protected SCIMAttribute createMultiValuedAttribute(
271          final JSONArray jsonAttribute,
272          final AttributeDescriptor attributeDescriptor)
273      throws JSONException, InvalidResourceException
274  {
275    final List<SCIMAttributeValue> values =
276        new ArrayList<SCIMAttributeValue>(jsonAttribute.length());
277
278    for (int i = 0; i < jsonAttribute.length(); i++)
279    {
280      Object o = jsonAttribute.get(i);
281      if (o.equals(JSONObject.NULL))
282      {
283        continue;
284      }
285
286      SCIMAttributeValue value;
287      if(o instanceof JSONObject)
288      {
289        value = createComplexAttribute((JSONObject) o, attributeDescriptor);
290      }
291      else
292      {
293        SCIMAttribute subAttr = SCIMAttribute.create(
294            attributeDescriptor.getSubAttribute("value"),
295            SCIMAttributeValue.createValue(attributeDescriptor.getDataType(),
296                                           o.toString()));
297        value = SCIMAttributeValue.createComplexValue(subAttr);
298      }
299      values.add(value);
300    }
301
302    if (values.isEmpty())
303    {
304      return null;
305    }
306
307    SCIMAttributeValue[] vals =
308        new SCIMAttributeValue[values.size()];
309    vals = values.toArray(vals);
310    return SCIMAttribute.create(attributeDescriptor, vals);
311  }
312
313
314
315  /**
316   * Parse a complex attribute from its representation as a JSON Object.
317   *
318   * @param jsonAttribute       The JSON object representing the attribute.
319   * @param attributeDescriptor The attribute descriptor.
320   *
321   * @return The parsed attribute.
322   *
323   * @throws org.json.JSONException Thrown if error creating complex attribute.
324   * @throws InvalidResourceException if a schema error occurs.
325   */
326  protected SCIMAttributeValue createComplexAttribute(
327      final JSONObject jsonAttribute,
328      final AttributeDescriptor attributeDescriptor)
329      throws JSONException, InvalidResourceException
330  {
331    final Iterator keys = jsonAttribute.keys();
332    final List<SCIMAttribute> complexAttrs =
333        new ArrayList<SCIMAttribute>(jsonAttribute.length());
334    while (keys.hasNext())
335    {
336      final String key = (String) keys.next();
337      final AttributeDescriptor subAttribute =
338          attributeDescriptor.getSubAttribute(key);
339      if (subAttribute != null)
340      {
341        SCIMAttribute childAttr = null;
342        // Allow multi-valued sub-attribute as the resource schema needs this.
343        if (subAttribute.isMultiValued())
344        {
345          final JSONArray o = jsonAttribute.getJSONArray(key);
346          childAttr = createMultiValuedAttribute(o, subAttribute);
347        }
348        else
349        {
350          final Object o = jsonAttribute.get(key);
351          if (!o.equals(JSONObject.NULL))
352          {
353            childAttr = createSimpleAttribute(o, subAttribute);
354          }
355        }
356        if (childAttr != null)
357        {
358          complexAttrs.add(childAttr);
359        }
360      }
361    }
362
363    return SCIMAttributeValue.createComplexValue(complexAttrs);
364  }
365
366
367
368  /**
369   * Create a SCIM attribute from its JSON object representation.
370   *
371   * @param descriptor     The attribute descriptor.
372   * @param jsonAttribute  The JSON object representing the attribute.
373   *
374   * @return  The created SCIM attribute, or {@code null} if the value is null.
375   *
376   * @throws JSONException If the JSON object is not valid.
377   * @throws InvalidResourceException If a schema error occurs.
378   */
379  protected SCIMAttribute create(
380      final AttributeDescriptor descriptor, final Object jsonAttribute)
381      throws JSONException, InvalidResourceException
382  {
383    if (jsonAttribute.equals(JSONObject.NULL))
384    {
385      return null;
386    }
387
388    if (descriptor.isMultiValued())
389    {
390      JSONArray jsonArray;
391      if (jsonAttribute instanceof JSONArray)
392      {
393        jsonArray = (JSONArray) jsonAttribute;
394      }
395      else
396      {
397        String[] s = new String[1];
398        s[0] = jsonAttribute.toString();
399        jsonArray = new JSONArray(s);
400      }
401
402      return createMultiValuedAttribute(jsonArray, descriptor);
403    }
404    else if (descriptor.getDataType() == AttributeDescriptor.DataType.COMPLEX)
405    {
406      if (!(jsonAttribute instanceof JSONObject))
407      {
408        throw new InvalidResourceException(
409            "JSON object expected for complex attribute '" +
410            descriptor.getName() + "'");
411      }
412      return SCIMAttribute.create(
413          descriptor,
414          createComplexAttribute((JSONObject) jsonAttribute, descriptor));
415    }
416    else
417    {
418      return this.createSimpleAttribute(jsonAttribute, descriptor);
419    }
420  }
421
422
423
424  /**
425   * Returns a copy of the specified JSONObject with all the keys lower-cased.
426   * This makes it much easier to use methods like JSONObject.opt() to find a
427   * key when you don't know what the case is.
428   *
429   * @param jsonObject the original JSON object.
430   * @return a new JSONObject with the keys all lower-cased.
431   * @throws JSONException if there is an error creating the new JSONObject.
432   */
433  static final JSONObject makeCaseInsensitive(final JSONObject jsonObject)
434          throws JSONException
435  {
436    if (jsonObject == null)
437    {
438      return null;
439    }
440
441    Iterator keys = jsonObject.keys();
442    Map lowerCaseMap = new HashMap(jsonObject.length());
443    while (keys.hasNext())
444    {
445      String key = keys.next().toString();
446      String lowerCaseKey = toLowerCase(key);
447      lowerCaseMap.put(lowerCaseKey, jsonObject.get(key));
448    }
449
450    return new JSONObject(lowerCaseMap);
451  }
452}