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    }