001package com.identityworksllc.iiq.common.vo;
002
003import com.fasterxml.jackson.core.JsonParser;
004import com.fasterxml.jackson.core.JsonProcessingException;
005import com.fasterxml.jackson.databind.DeserializationContext;
006import com.fasterxml.jackson.databind.JsonNode;
007import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
008import sailpoint.api.SailPointContext;
009import sailpoint.api.SailPointFactory;
010import sailpoint.tools.GeneralException;
011import sailpoint.tools.xml.XMLObjectFactory;
012
013import java.io.IOException;
014import java.util.Arrays;
015import java.util.HashSet;
016import java.util.Set;
017
018/**
019 * A Jackson de-serializer for an IIQ object. The input is expected to
020 * have a 'type' and an 'xml'. The XML will be parsed using Sailpoint's
021 * default {@link sailpoint.tools.xml.XMLObjectFactory} parser into an
022 * object of the given type.
023 */
024public class IIQObjectDeserializer extends StdDeserializer<Object> {
025    /**
026     * The list of valid types apart from `sailpoint.object.*` classes
027     */
028    private static final Set<String> VALID_TYPES = new HashSet<>(Arrays.asList(
029            "java.util.Map",
030            "java.util.List",
031            "java.lang.String",
032            "java.lang.Boolean",
033            "java.util.Date"
034    ));
035
036    /**
037     * The constructor expected by Jackson
038     */
039    protected IIQObjectDeserializer() {
040        this(null);
041    }
042
043    /**
044     * The constructor expected by Jackson, providing the type of this object
045     * @param t This type
046     */
047    public IIQObjectDeserializer(Class<Object> t) {
048        super(t);
049    }
050
051    @Override
052    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
053        JsonNode node = p.getCodec().readTree(p);
054        if (node.isObject()) {
055            JsonNode xml = node.get("xml");
056            JsonNode type = node.get("type");
057
058            String typeStr = type.asText();
059            if (typeStr == null || typeStr.trim().isEmpty()) {
060                throw new IOException("Unable to read a serialized IIQObject without a type");
061            }
062            if (typeStr.equals("null") || xml.isNull()) {
063                return null;
064            }
065            if (typeStr.startsWith("sailpoint.object") || VALID_TYPES.contains(typeStr)) {
066                String xmlStr = xml.asText();
067
068                try {
069                    SailPointContext context = SailPointFactory.getCurrentContext();
070                    if (context == null) {
071                        // TODO create a private context here?
072                        throw new IOException("IIQObject JSON must be deserialized in a SailPointContext session");
073                    }
074                    return XMLObjectFactory.getInstance().parseXml(context, xmlStr, true);
075                } catch(GeneralException e) {
076                    throw new IOException(e);
077                }
078            } else {
079                throw new IOException("Unexpected IIQObject type: " + typeStr);
080            }
081        } else if (node.isNull()) {
082            return null;
083        } else {
084            throw new IOException("Unexpected JsonNode type: " + node.getNodeType());
085        }
086    }
087
088}