001/*
002 * Copyright 2012-2013 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.ResourceDescriptor;
024import com.unboundid.scim.sdk.InvalidResourceException;
025import com.unboundid.scim.sdk.SCIMAttribute;
026import com.unboundid.scim.sdk.SCIMAttributeValue;
027import com.unboundid.scim.sdk.SCIMConstants;
028import com.unboundid.scim.sdk.SCIMObject;
029import org.json.JSONArray;
030import org.json.JSONException;
031import org.json.JSONObject;
032
033import java.util.ArrayList;
034import java.util.HashMap;
035import java.util.HashSet;
036import java.util.Iterator;
037import java.util.List;
038import java.util.Map;
039import java.util.Set;
040
041import static com.unboundid.scim.sdk.StaticUtils.toLowerCase;
042
043
044
045/**
046 * Helper class for JSON unmarshalling.
047 */
048public class JsonParser
049{
050  /**
051   * Read a SCIM resource from the specified JSON object.
052   *
053   * @param <R> The type of resource instance.
054   * @param jsonObject  The JSON object to be read.
055   * @param resourceDescriptor The descriptor of the SCIM resource to be read.
056   * @param resourceFactory The resource factory to use to create the resource
057   *                        instance.
058   * @param defaultSchemas  The set of schemas used by attributes of the
059   *                        resource, or {@code null} if the schemas must be
060   *                        provided in the resource object.
061   *
062   * @return  The SCIM resource that was read.
063   *
064   * @throws JSONException If an error occurred.
065   * @throws InvalidResourceException if a schema error occurs.
066   */
067  protected <R extends BaseResource> R unmarshal(
068      final JSONObject jsonObject,
069      final ResourceDescriptor resourceDescriptor,
070      final ResourceFactory<R> resourceFactory,
071      final JSONArray defaultSchemas)
072      throws JSONException, InvalidResourceException
073  {
074    try
075    {
076      final SCIMObject scimObject = new SCIMObject();
077
078      // The first keyed object ought to be a schemas array, but it may not be
079      // present if 1) the attrs are all core and 2) the client decided to omit
080      // the schema declaration.
081      final JSONArray schemas;
082      if (jsonObject.has(SCIMConstants.SCHEMAS_ATTRIBUTE_NAME))
083      {
084        schemas = jsonObject.getJSONArray(SCIMConstants.SCHEMAS_ATTRIBUTE_NAME);
085      }
086      else if (defaultSchemas != null)
087      {
088        schemas = defaultSchemas;
089      }
090      else
091      {
092        String[] schemaArray = new String[1];
093        schemaArray[0] = resourceDescriptor.getSchema();
094        schemas = new JSONArray(schemaArray);
095      }
096
097      final Set<String> schemaSet = new HashSet<String>(schemas.length());
098      for (int i = 0; i < schemas.length(); i++)
099      {
100        schemaSet.add(toLowerCase(schemas.getString(i)));
101      }
102
103      final Iterator k = jsonObject.keys();
104      while (k.hasNext())
105      {
106        final String attributeKey = (String) k.next();
107        final String attributeKeyLower = toLowerCase(attributeKey);
108
109        if(SCIMConstants.SCHEMAS_ATTRIBUTE_NAME.equals(attributeKeyLower))
110        {
111          continue;
112        }
113
114        if (schemaSet.contains(attributeKeyLower))
115        {
116          //This key is a container for some extended schema
117          JSONObject schemaAttrs = jsonObject.getJSONObject(attributeKey);
118          final Iterator keys = schemaAttrs.keys();
119          while (keys.hasNext())
120          {
121            final String attributeName = (String) keys.next();
122            final AttributeDescriptor attributeDescriptor =
123                   resourceDescriptor.getAttribute(attributeKey, attributeName);
124            final Object jsonAttribute = schemaAttrs.get(attributeName);
125            scimObject.addAttribute(
126                    create(attributeDescriptor, jsonAttribute));
127          }
128        }
129        else
130        {
131          if (!schemaSet.contains(SCIMConstants.SCHEMA_URI_CORE))
132          {
133            throw new Exception("'" + SCIMConstants.SCHEMA_URI_CORE +
134                    "' must be declared in the schemas attribute.");
135          }
136          final Object jsonAttribute = jsonObject.get(attributeKey);
137          final AttributeDescriptor attributeDescriptor =
138                  resourceDescriptor.getAttribute(SCIMConstants.SCHEMA_URI_CORE,
139                          attributeKey);
140          scimObject.addAttribute(create(attributeDescriptor, jsonAttribute));
141        }
142      }
143
144      return resourceFactory.createResource(resourceDescriptor, scimObject);
145    }
146    catch (Exception e)
147    {
148      throw new InvalidResourceException(
149          "Resource '" + resourceDescriptor.getName() + "' is malformed: " +
150          e.getMessage());
151    }
152  }
153
154
155
156  /**
157   * Parse a simple attribute from its representation as a JSON Object.
158   *
159   * @param jsonAttribute       The JSON object representing the attribute.
160   * @param attributeDescriptor The attribute descriptor.
161   *
162   * @return The parsed attribute.
163   */
164  protected SCIMAttribute createSimpleAttribute(
165      final Object jsonAttribute,
166      final AttributeDescriptor attributeDescriptor)
167  {
168    return SCIMAttribute.create(attributeDescriptor,
169        SCIMAttributeValue.createValue(attributeDescriptor.getDataType(),
170                                       jsonAttribute.toString()));
171  }
172
173
174
175  /**
176   * Parse a multi-valued attribute from its representation as a JSON Object.
177   *
178   * @param jsonAttribute       The JSON object representing the attribute.
179   * @param attributeDescriptor The attribute descriptor.
180   *
181   * @return The parsed attribute.
182   *
183   * @throws JSONException Thrown if error creating multi-valued attribute.
184   * @throws InvalidResourceException if a schema error occurs.
185   */
186  protected SCIMAttribute createMutiValuedAttribute(
187      final JSONArray jsonAttribute,
188      final AttributeDescriptor attributeDescriptor)
189      throws JSONException, InvalidResourceException
190  {
191    final List<SCIMAttributeValue> values =
192        new ArrayList<SCIMAttributeValue>(jsonAttribute.length());
193
194    for (int i = 0; i < jsonAttribute.length(); i++)
195    {
196      Object o = jsonAttribute.get(i);
197      SCIMAttributeValue value;
198      if(o instanceof JSONObject)
199      {
200        value = createComplexAttribute((JSONObject) o, attributeDescriptor);
201      }
202      else
203      {
204        SCIMAttribute subAttr = SCIMAttribute.create(
205            attributeDescriptor.getSubAttribute("value"),
206            SCIMAttributeValue.createValue(attributeDescriptor.getDataType(),
207                                           o.toString()));
208        value = SCIMAttributeValue.createComplexValue(subAttr);
209      }
210      values.add(value);
211    }
212    SCIMAttributeValue[] vals =
213        new SCIMAttributeValue[values.size()];
214    vals = values.toArray(vals);
215    return SCIMAttribute.create(attributeDescriptor, vals);
216  }
217
218
219
220  /**
221   * Parse a complex attribute from its representation as a JSON Object.
222   *
223   * @param jsonAttribute       The JSON object representing the attribute.
224   * @param attributeDescriptor The attribute descriptor.
225   *
226   * @return The parsed attribute.
227   *
228   * @throws org.json.JSONException Thrown if error creating complex attribute.
229   * @throws InvalidResourceException if a schema error occurs.
230   */
231  protected SCIMAttributeValue createComplexAttribute(
232      final JSONObject jsonAttribute,
233      final AttributeDescriptor attributeDescriptor)
234      throws JSONException, InvalidResourceException
235  {
236    final Iterator keys = jsonAttribute.keys();
237    final List<SCIMAttribute> complexAttrs =
238        new ArrayList<SCIMAttribute>(jsonAttribute.length());
239    while (keys.hasNext())
240    {
241      final String key = (String) keys.next();
242      final AttributeDescriptor subAttribute =
243          attributeDescriptor.getSubAttribute(key);
244      if (subAttribute != null)
245      {
246        SCIMAttribute childAttr;
247        // Allow multi-valued sub-attribute as the resource schema needs this.
248        if (subAttribute.isMultiValued())
249        {
250          final JSONArray o = jsonAttribute.getJSONArray(key);
251          childAttr = createMutiValuedAttribute(o, subAttribute);
252        }
253        else
254        {
255          final Object o = jsonAttribute.get(key);
256          childAttr = createSimpleAttribute(o, subAttribute);
257        }
258        complexAttrs.add(childAttr);
259      }
260    }
261
262    return SCIMAttributeValue.createComplexValue(complexAttrs);
263  }
264
265
266
267  /**
268   * Create a SCIM attribute from its JSON object representation.
269   *
270   * @param descriptor     The attribute descriptor.
271   * @param jsonAttribute  The JSON object representing the attribute.
272   *
273   * @return  The created SCIM attribute.
274   *
275   * @throws JSONException If the JSON object is not valid.
276   * @throws InvalidResourceException If a schema error occurs.
277   */
278  protected SCIMAttribute create(
279      final AttributeDescriptor descriptor, final Object jsonAttribute)
280      throws JSONException, InvalidResourceException
281  {
282    if (descriptor.isMultiValued())
283    {
284      JSONArray jsonArray;
285      if (jsonAttribute instanceof JSONArray)
286      {
287        jsonArray = (JSONArray) jsonAttribute;
288      }
289      else
290      {
291        String[] s = new String[1];
292        s[0] = jsonAttribute.toString();
293        jsonArray = new JSONArray(s);
294      }
295
296      return createMutiValuedAttribute(jsonArray, descriptor);
297    }
298    else if (descriptor.getDataType() == AttributeDescriptor.DataType.COMPLEX)
299    {
300      if (!(jsonAttribute instanceof JSONObject))
301      {
302        throw new InvalidResourceException(
303            "JSON object expected for complex attribute '" +
304            descriptor.getName() + "'");
305      }
306      return SCIMAttribute.create(
307          descriptor,
308          createComplexAttribute((JSONObject) jsonAttribute, descriptor));
309    }
310    else
311    {
312      return this.createSimpleAttribute(jsonAttribute, descriptor);
313    }
314  }
315
316
317
318  /**
319   * Returns a copy of the specified JSONObject with all the keys lower-cased.
320   * This makes it much easier to use methods like JSONObject.opt() to find a
321   * key when you don't know what the case is.
322   *
323   * @param jsonObject the original JSON object.
324   * @return a new JSONObject with the keys all lower-cased.
325   * @throws JSONException if there is an error creating the new JSONObject.
326   */
327  static final JSONObject makeCaseInsensitive(final JSONObject jsonObject)
328          throws JSONException
329  {
330    if (jsonObject == null)
331    {
332      return null;
333    }
334
335    Iterator keys = jsonObject.keys();
336    Map lowerCaseMap = new HashMap(jsonObject.length());
337    while (keys.hasNext())
338    {
339      String key = keys.next().toString();
340      String lowerCaseKey = toLowerCase(key);
341      lowerCaseMap.put(lowerCaseKey, jsonObject.get(key));
342    }
343
344    return new JSONObject(lowerCaseMap);
345  }
346}