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.BulkConfig; 022import com.unboundid.scim.marshal.BulkInputStreamWrapper; 023import com.unboundid.scim.schema.AttributeDescriptor; 024import com.unboundid.scim.schema.ResourceDescriptor; 025import com.unboundid.scim.sdk.BulkContentHandler; 026import com.unboundid.scim.sdk.BulkException; 027import com.unboundid.scim.sdk.BulkOperation; 028import com.unboundid.scim.sdk.BulkOperation.Method; 029import com.unboundid.scim.sdk.Debug; 030import com.unboundid.scim.sdk.InvalidResourceException; 031import com.unboundid.scim.sdk.SCIMAttribute; 032import com.unboundid.scim.sdk.SCIMAttributeValue; 033import com.unboundid.scim.sdk.SCIMException; 034import com.unboundid.scim.sdk.Status; 035import org.json.JSONException; 036import org.json.JSONObject; 037import org.json.JSONTokener; 038 039import java.io.InputStream; 040 041 042 043/** 044 * This class is a helper class to handle parsing of JSON bulk operations. 045 */ 046public class JsonBulkParser extends JsonParser 047{ 048 private final BulkInputStreamWrapper bulkInputStream; 049 private final BulkConfig bulkConfig; 050 private final BulkContentHandler handler; 051 private int operationIndex = 0; 052 private JSONTokener tokener; 053 private boolean skipOperations; 054 055 /** 056 * Create a new instance of this bulk unmarshaller. 057 * 058 * @param inputStream The input stream containing the bulk content to be 059 * read. 060 * @param bulkConfig The bulk configuration settings to be enforced. 061 * @param handler A bulk operation listener to handle the content as it 062 * is read. 063 */ 064 public JsonBulkParser(final InputStream inputStream, 065 final BulkConfig bulkConfig, 066 final BulkContentHandler handler) 067 { 068 this.bulkInputStream = new BulkInputStreamWrapper(inputStream); 069 this.bulkConfig = bulkConfig; 070 this.handler = handler; 071 this.operationIndex = 0; 072 } 073 074 075 076 /** 077 * Specify whether bulk operations should be skipped. 078 * 079 * @param skipOperations {@code true} if bulk operations should be skipped. 080 */ 081 public void setSkipOperations(final boolean skipOperations) 082 { 083 this.skipOperations = skipOperations; 084 } 085 086 087 088 /** 089 * Reads a SCIM bulk request or response from the input stream. 090 * 091 * @throws SCIMException If the bulk content could not be read. 092 */ 093 public void unmarshal() 094 throws SCIMException 095 { 096 try 097 { 098 tokener = new JSONTokener(bulkInputStream); 099 100 if (tokener.nextClean() != '{') 101 { 102 throw tokener.syntaxError("A JSONObject text must begin with '{'"); 103 } 104 for (;;) 105 { 106 String key; 107 char c = tokener.nextClean(); 108 switch (c) 109 { 110 case 0: 111 throw tokener.syntaxError("A JSONObject text must end with '}'"); 112 case '}': 113 return; 114 default: 115 tokener.back(); 116 key = tokener.nextValue().toString(); 117 } 118 119 // The key is followed by ':'. We will also tolerate '=' or '=>'. 120 121 c = tokener.nextClean(); 122 if (c == '=') 123 { 124 if (tokener.next() != '>') 125 { 126 tokener.back(); 127 } 128 } else if (c != ':') 129 { 130 throw tokener.syntaxError("Expected a ':' after a key"); 131 } 132 133 if (key.equals("failOnErrors")) 134 { 135 handler.handleFailOnErrors((Integer)tokener.nextValue()); 136 } 137 else if (key.equals("Operations")) 138 { 139 parseOperations(); 140 } 141 else 142 { 143 // Skip. 144 tokener.nextValue(); 145 } 146 147 // Pairs are separated by ','. We will also tolerate ';'. 148 149 switch (tokener.nextClean()) 150 { 151 case ';': 152 case ',': 153 if (tokener.nextClean() == '}') 154 { 155 return; 156 } 157 tokener.back(); 158 break; 159 case '}': 160 return; 161 default: 162 throw tokener.syntaxError("Expected a ',' or '}'"); 163 } 164 } 165 } 166 catch (SCIMException e) 167 { 168 throw e; 169 } 170 catch (Exception e) 171 { 172 Debug.debugException(e); 173 throw new InvalidResourceException( 174 "Error while reading JSON Bulk content: " + e.getMessage(), e); 175 } 176 } 177 178 179 180 /** 181 * Parse the Operations element. 182 * 183 * @throws JSONException If the JSON could not be parsed. 184 * @throws SCIMException If some other error occurred. 185 */ 186 private void parseOperations() 187 throws JSONException, SCIMException 188 { 189 if (tokener.nextClean() != '[') 190 { 191 throw this.tokener.syntaxError("A JSONArray text must start with '['"); 192 } 193 if (tokener.nextClean() != ']') 194 { 195 tokener.back(); 196 for (;;) 197 { 198 if (tokener.nextClean() != ',') 199 { 200 tokener.back(); 201 if (operationIndex >= bulkConfig.getMaxOperations()) 202 { 203 throw SCIMException.createException( 204 413, 205 "The number of operations in the bulk operation exceeds " + 206 "maxOperations (" + bulkConfig.getMaxOperations() + ")"); 207 } 208 209 if (bulkInputStream.getBytesRead() > bulkConfig.getMaxPayloadSize()) 210 { 211 throw SCIMException.createException( 212 413, 213 "The size of the bulk operation exceeds the maxPayloadSize " + 214 "(" + bulkConfig.getMaxPayloadSize() + ")"); 215 } 216 if (skipOperations) 217 { 218 tokener.nextValue(); 219 } 220 else 221 { 222 JSONObject o = makeCaseInsensitive((JSONObject)tokener.nextValue()); 223 try 224 { 225 handler.handleOperation(operationIndex, parseBulkOperation(o)); 226 } 227 catch (BulkException e) 228 { 229 handler.handleException(operationIndex, e); 230 } 231 } 232 operationIndex++; 233 } 234 235 switch (tokener.nextClean()) 236 { 237 case ';': 238 case ',': 239 if (tokener.nextClean() == ']') 240 { 241 return; 242 } 243 tokener.back(); 244 break; 245 case ']': 246 return; 247 default: 248 throw tokener.syntaxError("Expected a ',' or ']'"); 249 } 250 } 251 } 252 } 253 254 255 256 /** 257 * Parse an individual operation in a bulk operation request or response. 258 * 259 * @param o The JSON object representing the operation. 260 * 261 * @return The parsed bulk operation. 262 * 263 * @throws BulkException If the operation cannot be parsed for some other 264 * reason. 265 */ 266 private BulkOperation parseBulkOperation(final JSONObject o) 267 throws BulkException 268 { 269 final String method = o.optString("method"); 270 final String bulkId = o.optString("bulkid", null); 271 final String version = o.optString("version", null); 272 final String path = o.optString("path", null); 273 final String location = o.optString("location", null); 274 275 Method httpMethod = null; 276 try 277 { 278 279 try 280 { 281 httpMethod = Method.valueOf(method.toUpperCase()); 282 } 283 catch (IllegalArgumentException e) 284 { 285 //This will be handled later on in 286 //BulkContentHandler.handleOperation(). 287 } 288 289 final JSONObject data = makeCaseInsensitive(o.optJSONObject("data")); 290 final JSONObject statusObj = 291 makeCaseInsensitive(o.optJSONObject("status")); 292 293 final Status status; 294 if (statusObj != null) 295 { 296 final String code = statusObj.getString("code"); 297 final String description = statusObj.optString("description", null); 298 status = new Status(code, description); 299 } 300 else 301 { 302 status = null; 303 } 304 305 BaseResource resource = null; 306 if (data != null) 307 { 308 if (path == null) 309 { 310 throw new BulkException(new InvalidResourceException( 311 "Bulk operation " + operationIndex + " has data but no path"), 312 httpMethod, bulkId, path); 313 } 314 315 int startPos = 0; 316 if (path.charAt(startPos) == '/') 317 { 318 startPos++; 319 } 320 321 int endPos = path.indexOf('/', startPos); 322 if (endPos == -1) 323 { 324 endPos = path.length(); 325 } 326 327 String endpoint = path.substring(startPos, endPos); 328 329 final ResourceDescriptor descriptor = 330 handler.getResourceDescriptor(endpoint); 331 if (descriptor == null) 332 { 333 throw new BulkException(new InvalidResourceException( 334 "Bulk operation " + operationIndex + " specifies an unknown " + 335 "resource endpoint '" + endpoint + "'"), 336 httpMethod, bulkId, path); 337 } 338 339 try 340 { 341 resource = unmarshal(data, descriptor, 342 BaseResource.BASE_RESOURCE_FACTORY, null); 343 } 344 catch (InvalidResourceException e) 345 { 346 throw new BulkException(e, httpMethod, bulkId, path); 347 } 348 } 349 350 return new BulkOperation(httpMethod, bulkId, version, path, location, 351 resource, status); 352 } 353 catch (JSONException e) 354 { 355 throw new BulkException(new InvalidResourceException( 356 "Bulk operation " + operationIndex + " is malformed: " + 357 e.getMessage()), 358 httpMethod, bulkId, path); 359 } 360 } 361 362 363 364 /** 365 * Parse a simple attribute from its representation as a JSON Object. 366 * 367 * @param jsonAttribute The JSON object representing the attribute. 368 * @param attributeDescriptor The attribute descriptor. 369 * 370 * @return The parsed attribute. 371 */ 372 protected SCIMAttribute createSimpleAttribute( 373 final Object jsonAttribute, 374 final AttributeDescriptor attributeDescriptor) 375 { 376 final String v = 377 handler.transformValue(operationIndex, jsonAttribute.toString()); 378 379 return SCIMAttribute.create( 380 attributeDescriptor, 381 SCIMAttributeValue.createValue(attributeDescriptor.getDataType(), v)); 382 } 383}