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