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}