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}