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