001 /*
002 * Copyright 2011-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.xml;
019
020 import com.unboundid.scim.data.BaseResource;
021 import com.unboundid.scim.data.BulkConfig;
022 import com.unboundid.scim.data.ResourceFactory;
023 import com.unboundid.scim.marshal.Unmarshaller;
024 import com.unboundid.scim.schema.AttributeDescriptor;
025 import com.unboundid.scim.schema.ResourceDescriptor;
026 import com.unboundid.scim.sdk.BulkContentHandler;
027 import com.unboundid.scim.sdk.Debug;
028 import com.unboundid.scim.sdk.InvalidResourceException;
029 import com.unboundid.scim.sdk.Resources;
030 import com.unboundid.scim.sdk.SCIMAttribute;
031 import com.unboundid.scim.sdk.SCIMAttributeValue;
032 import com.unboundid.scim.sdk.SCIMException;
033 import com.unboundid.scim.sdk.SCIMObject;
034 import com.unboundid.scim.sdk.ServerErrorException;
035 import org.w3c.dom.Document;
036 import org.w3c.dom.Element;
037 import org.w3c.dom.Node;
038 import org.w3c.dom.NodeList;
039
040 import javax.xml.parsers.DocumentBuilder;
041 import javax.xml.parsers.DocumentBuilderFactory;
042 import java.io.BufferedInputStream;
043 import java.io.File;
044 import java.io.FileInputStream;
045 import java.io.IOException;
046 import java.io.InputStream;
047 import java.util.ArrayList;
048 import java.util.Collections;
049 import java.util.List;
050
051
052
053 /**
054 * This class provides a SCIM object un-marshaller implementation to read SCIM
055 * objects from their XML representation.
056 */
057 public class XmlUnmarshaller implements Unmarshaller
058 {
059 /**
060 * {@inheritDoc}
061 */
062 public <R extends BaseResource> R unmarshal(
063 final InputStream inputStream,
064 final ResourceDescriptor resourceDescriptor,
065 final ResourceFactory<R> resourceFactory)
066 throws InvalidResourceException
067 {
068 final Document doc;
069 try
070 {
071 final DocumentBuilderFactory dbFactory =
072 DocumentBuilderFactory.newInstance();
073 dbFactory.setNamespaceAware(true);
074 dbFactory.setIgnoringElementContentWhitespace(true);
075 dbFactory.setValidating(false);
076 final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
077 doc = dBuilder.parse(inputStream);
078 doc.getDocumentElement().normalize();
079 }
080 catch (Exception e)
081 {
082 throw new InvalidResourceException("Error reading XML: " +
083 e.getMessage(), e);
084 }
085
086 final Element documentElement = doc.getDocumentElement();
087
088 // TODO: Should we check to make sure the doc name matches the
089 // resource name?
090 //documentElement.getLocalName());
091 if (resourceDescriptor == null)
092 {
093 throw new RuntimeException("No resource descriptor found for " +
094 documentElement.getLocalName());
095 }
096
097 final String documentNamespaceURI = documentElement.getNamespaceURI();
098 return unmarshal(documentNamespaceURI, documentElement.getChildNodes(),
099 resourceDescriptor, resourceFactory);
100 }
101
102 /**
103 * Read an SCIM resource from the specified node.
104 *
105 * @param documentNamespaceURI The namespace URI of XML document.
106 * @param <R> The type of resource instance.
107 * @param nodeList The attribute nodes to be read.
108 * @param resourceDescriptor The descriptor of the SCIM resource to be read.
109 * @param resourceFactory The resource factory to use to create the resource
110 * instance.
111 *
112 * @return The SCIM resource that was read.
113 * @throws com.unboundid.scim.sdk.InvalidResourceException if an error occurs.
114 */
115 private <R extends BaseResource> R unmarshal(
116 final String documentNamespaceURI,
117 final NodeList nodeList, final ResourceDescriptor resourceDescriptor,
118 final ResourceFactory<R> resourceFactory) throws InvalidResourceException
119 {
120 SCIMObject scimObject = new SCIMObject();
121 for (int i = 0; i < nodeList.getLength(); i++)
122 {
123 final Node element = nodeList.item(i);
124 if(element.getNodeType() != Node.ELEMENT_NODE)
125 {
126 continue;
127 }
128
129 String namespaceURI = element.getNamespaceURI();
130 if (namespaceURI == null)
131 {
132 namespaceURI = documentNamespaceURI; // TODO: not sure about this
133 }
134
135 final AttributeDescriptor attributeDescriptor =
136 resourceDescriptor.getAttribute(namespaceURI, element.getLocalName());
137
138 final SCIMAttribute attr;
139 if (attributeDescriptor.isMultiValued())
140 {
141 attr = createMultiValuedAttribute(element, attributeDescriptor);
142 }
143 else if (attributeDescriptor.getDataType() ==
144 AttributeDescriptor.DataType.COMPLEX)
145 {
146 attr = SCIMAttribute.create(attributeDescriptor,
147 createComplexAttribute(element, attributeDescriptor));
148 }
149 else
150 {
151 attr = createSimpleAttribute(element, attributeDescriptor);
152 }
153
154 scimObject.addAttribute(attr);
155 }
156 return resourceFactory.createResource(resourceDescriptor, scimObject);
157 }
158
159 /**
160 * {@inheritDoc}
161 */
162 public <R extends BaseResource> Resources<R> unmarshalResources(
163 final InputStream inputStream,
164 final ResourceDescriptor resourceDescriptor,
165 final ResourceFactory<R> resourceFactory) throws InvalidResourceException
166 {
167 final Document doc;
168 try
169 {
170 final DocumentBuilderFactory dbFactory =
171 DocumentBuilderFactory.newInstance();
172 dbFactory.setNamespaceAware(true);
173 dbFactory.setIgnoringElementContentWhitespace(true);
174 dbFactory.setValidating(false);
175 final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
176 doc = dBuilder.parse(inputStream);
177 doc.getDocumentElement().normalize();
178 }
179 catch (Exception e)
180 {
181 throw new InvalidResourceException("Error reading XML: " +
182 e.getMessage(), e);
183 }
184
185 final String documentNamespaceURI =
186 doc.getDocumentElement().getNamespaceURI();
187 final NodeList nodeList = doc.getElementsByTagName("*");
188
189 int totalResults = 0;
190 int startIndex = 1;
191 List<R> objects = Collections.emptyList();
192 for (int i = 0; i < nodeList.getLength(); i++)
193 {
194 final Node element = nodeList.item(i);
195 if(element.getLocalName().equals("totalResults"))
196 {
197 totalResults = Integer.valueOf(element.getTextContent());
198 }
199 else if(element.getLocalName().equals("startIndex"))
200 {
201 startIndex = Integer.valueOf(element.getTextContent());
202 }
203 else if(element.getLocalName().equals("Resources"))
204 {
205 NodeList resources = element.getChildNodes();
206 objects = new ArrayList<R>(resources.getLength());
207 for(int j = 0; j < resources.getLength(); j++)
208 {
209 Node resource = resources.item(j);
210 if(resource.getLocalName().equals("Resource"))
211 {
212 objects.add(
213 unmarshal(documentNamespaceURI, resource.getChildNodes(),
214 resourceDescriptor, resourceFactory));
215 }
216 }
217 }
218 }
219
220 return new Resources<R>(objects, totalResults, startIndex);
221 }
222
223 /**
224 * {@inheritDoc}
225 */
226 public SCIMException unmarshalError(final InputStream inputStream)
227 throws InvalidResourceException
228 {
229 final Document doc;
230 try
231 {
232 final DocumentBuilderFactory dbFactory =
233 DocumentBuilderFactory.newInstance();
234 dbFactory.setNamespaceAware(true);
235 dbFactory.setIgnoringElementContentWhitespace(true);
236 dbFactory.setValidating(false);
237 final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
238 doc = dBuilder.parse(inputStream);
239 doc.getDocumentElement().normalize();
240 }
241 catch (Exception e)
242 {
243 throw new InvalidResourceException("Error reading XML: " +
244 e.getMessage(), e);
245 }
246
247 final NodeList nodeList =
248 doc.getDocumentElement().getFirstChild().getChildNodes();
249
250 if(nodeList.getLength() >= 1)
251 {
252 String code = null;
253 String description = null;
254 NodeList nodes = nodeList.item(0).getChildNodes();
255 for(int j = 0; j < nodes.getLength(); j++)
256 {
257 Node attr = nodes.item(j);
258 if(attr.getLocalName().equals("code"))
259 {
260 code = attr.getTextContent();
261 }
262 else if(attr.getLocalName().equals("description"))
263 {
264 description = attr.getTextContent();
265 }
266 }
267 return SCIMException.createException(Integer.valueOf(code),
268 description);
269 }
270
271 return null;
272
273 }
274
275
276
277 /**
278 * {@inheritDoc}
279 */
280 public void bulkUnmarshal(final InputStream inputStream,
281 final BulkConfig bulkConfig,
282 final BulkContentHandler handler)
283 throws SCIMException
284 {
285 final XmlBulkParser xmlBulkParser =
286 new XmlBulkParser(inputStream, bulkConfig, handler);
287 xmlBulkParser.unmarshal();
288 }
289
290
291
292 /**
293 * {@inheritDoc}
294 */
295 public void bulkUnmarshal(final File file,
296 final BulkConfig bulkConfig,
297 final BulkContentHandler handler)
298 throws SCIMException
299 {
300 // First pass: ensure the number of operations is less than the max.
301 final BulkContentHandler preProcessHandler = new BulkContentHandler() {};
302 try
303 {
304 final FileInputStream fileInputStream = new FileInputStream(file);
305 try
306 {
307 final BufferedInputStream bufferedInputStream =
308 new BufferedInputStream(fileInputStream);
309 try
310 {
311 final XmlBulkParser xmlBulkParser =
312 new XmlBulkParser(bufferedInputStream, bulkConfig,
313 preProcessHandler);
314 xmlBulkParser.setSkipOperations(true);
315 xmlBulkParser.unmarshal();
316 }
317 finally
318 {
319 bufferedInputStream.close();
320 }
321 }
322 finally
323 {
324 fileInputStream.close();
325 }
326 }
327 catch (IOException e)
328 {
329 Debug.debugException(e);
330 throw new ServerErrorException(
331 "Error pre-processing bulk request: " + e.getMessage());
332 }
333
334 // Second pass: Parse fully.
335 try
336 {
337 final FileInputStream fileInputStream = new FileInputStream(file);
338 try
339 {
340 final BufferedInputStream bufferedInputStream =
341 new BufferedInputStream(fileInputStream);
342 try
343 {
344 final XmlBulkParser xmlBulkParser =
345 new XmlBulkParser(bufferedInputStream, bulkConfig, handler);
346 xmlBulkParser.unmarshal();
347 }
348 finally
349 {
350 bufferedInputStream.close();
351 }
352 }
353 finally
354 {
355 fileInputStream.close();
356 }
357 }
358 catch (IOException e)
359 {
360 Debug.debugException(e);
361 throw new ServerErrorException(
362 "Error parsing bulk request: " + e.getMessage());
363 }
364 }
365
366
367
368 /**
369 * Parse a simple attribute from its representation as a DOM node.
370 *
371 * @param node The DOM node representing the attribute.
372 * @param attributeDescriptor The attribute descriptor.
373 *
374 * @return The parsed attribute.
375 */
376 private SCIMAttribute createSimpleAttribute(
377 final Node node,
378 final AttributeDescriptor attributeDescriptor)
379 {
380 return SCIMAttribute.create(attributeDescriptor,
381 SCIMAttributeValue.createValue(attributeDescriptor.getDataType(),
382 node.getTextContent()));
383 }
384
385
386
387 /**
388 * Parse a multi-valued attribute from its representation as a DOM node.
389 *
390 * @param node The DOM node representing the attribute.
391 * @param attributeDescriptor The attribute descriptor.
392 *
393 * @return The parsed attribute.
394 * @throws InvalidResourceException if an error occurs.
395 */
396 private SCIMAttribute createMultiValuedAttribute(
397 final Node node, final AttributeDescriptor attributeDescriptor)
398 throws InvalidResourceException
399 {
400 final NodeList attributes = node.getChildNodes();
401 final List<SCIMAttributeValue> values =
402 new ArrayList<SCIMAttributeValue>(attributes.getLength());
403 for (int i = 0; i < attributes.getLength(); i++)
404 {
405 final Node attribute = attributes.item(i);
406 if (attribute.getNodeType() != Node.ELEMENT_NODE ||
407 !attribute.getLocalName().equals(
408 attributeDescriptor.getMultiValuedChildName()))
409 {
410 continue;
411 }
412 values.add(
413 createComplexAttribute(attribute, attributeDescriptor));
414 }
415 SCIMAttributeValue[] vals =
416 new SCIMAttributeValue[values.size()];
417 vals = values.toArray(vals);
418 return SCIMAttribute.create(attributeDescriptor, vals);
419 }
420
421
422
423 /**
424 * Parse a complex attribute from its representation as a DOM node.
425 *
426 * @param node The DOM node representing the attribute.
427 * @param attributeDescriptor The attribute descriptor.
428 *
429 * @return The parsed attribute.
430 * @throws InvalidResourceException if an error occurs.
431 */
432 private SCIMAttributeValue createComplexAttribute(
433 final Node node, final AttributeDescriptor attributeDescriptor)
434 throws InvalidResourceException
435 {
436 NodeList childNodes = node.getChildNodes();
437 List<SCIMAttribute> complexAttrs =
438 new ArrayList<SCIMAttribute>(childNodes.getLength());
439 for (int i = 0; i < childNodes.getLength(); i++)
440 {
441 Node item1 = childNodes.item(i);
442 if (item1.getNodeType() == Node.ELEMENT_NODE)
443 {
444 if(item1.getNamespaceURI() != null &&
445 !item1.getNamespaceURI().equalsIgnoreCase(
446 attributeDescriptor.getSchema()))
447 {
448 // Sub-attributes should have the same namespace URI as the complex
449 // attribute.
450 throw new InvalidResourceException("Sub-attribute " +
451 item1.getNodeName() + " does not use the same namespace as the " +
452 "containing complex attribute " + attributeDescriptor.getName());
453 }
454 SCIMAttribute childAttr;
455 AttributeDescriptor subAttribute =
456 attributeDescriptor.getSubAttribute(item1.getLocalName());
457 // Allow multi-valued sub-attribute as the resource schema needs this.
458 if(subAttribute.isMultiValued())
459 {
460 childAttr = createMultiValuedAttribute(item1, subAttribute);
461 }
462 else
463 {
464 childAttr = createSimpleAttribute(item1, subAttribute);
465 }
466 complexAttrs.add(childAttr);
467 }
468 }
469
470 return SCIMAttributeValue.createComplexValue(complexAttrs);
471 }
472 }