ServiceBusManagementSerializer.java

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.messaging.servicebus.implementation;

import com.azure.core.http.HttpHeaders;
import com.azure.core.util.logging.ClientLogger;
import com.azure.core.util.serializer.CollectionFormat;
import com.azure.core.util.serializer.JacksonAdapter;
import com.azure.core.util.serializer.SerializerAdapter;
import com.azure.core.util.serializer.SerializerEncoding;
import com.azure.messaging.servicebus.implementation.models.CreateQueueBody;
import com.azure.messaging.servicebus.implementation.models.CreateRuleBody;
import com.azure.messaging.servicebus.implementation.models.CreateSubscriptionBody;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Serializes and deserializes data plane responses from Service Bus.
 */
public class ServiceBusManagementSerializer implements SerializerAdapter {
    private static final String MINIMUM_DATETIME_FORMATTED = ">0001-01-01T00:00:00Z</";
    private static final Pattern MINIMUM_DATETIME_PATTERN = Pattern.compile(">0001-01-01T00:00:00</",
        Pattern.MULTILINE);
    private static final Pattern NAMESPACE_PATTERN = Pattern.compile(
        "xmlns:(?<namespace>\\w+)=\"http://schemas\\.microsoft\\.com/netservices/2010/10/servicebus/connect\"",
        Pattern.MULTILINE);
    private static final Pattern FILTER_ACTION_PATTERN = Pattern.compile("<(Filter|Action) type=",
        Pattern.MULTILINE);

    private final JacksonAdapter jacksonAdapter = new JacksonAdapter();
    private final ClientLogger logger = new ClientLogger(ServiceBusManagementSerializer.class);

    @Override
    public String serialize(Object object, SerializerEncoding encoding) throws IOException {
        final String contents = jacksonAdapter.serialize(object, encoding);

        final Class<?> clazz = object.getClass();
        if (!CreateQueueBody.class.equals(clazz) && !CreateRuleBody.class.equals(clazz)
            && !CreateSubscriptionBody.class.equals(clazz)) {
            return contents;
        }

        // This hack exists because the service requires a global namespace for the XML rather than allowing
        // each XML element to be prefaced with an explicit namespace. For example:
        // xmlns="foo" works because "foo" is assigned the global namespace.
        // xmlns:ns0="foo", and then prefixing all elements with ns0:AuthorizationRule will break.
        final Matcher namespaceMatcher = NAMESPACE_PATTERN.matcher(contents);
        if (!namespaceMatcher.find()) {
            logger.warning("Could not find {} in {}", NAMESPACE_PATTERN.pattern(), contents);
            return contents;
        }

        final String namespace = namespaceMatcher.group("namespace");
        final String replaced = contents
            .replaceAll(namespace + ":", "")
            .replace("xmlns:" + namespace + "=", "xmlns=");

        if (!CreateRuleBody.class.equals(clazz)) {
            return replaced;
        }

        // This hack is here because RuleFilter and RuleAction type="Foo" should have a namespace like n0:type="Foo".
        final Matcher filterType = FILTER_ACTION_PATTERN.matcher(replaced);
        if (filterType.find()) {
            return filterType.replaceAll("<$1 xmlns:ns0=\"http://www.w3.org/2001/XMLSchema-instance\" ns0:type=");
        } else {
            logger.warning("Could not find filter name pattern '{}' in {}.", FILTER_ACTION_PATTERN.pattern(),
                contents);
            return replaced;
        }
    }

    @Override
    public String serializeRaw(Object object) {
        return jacksonAdapter.serializeRaw(object);
    }

    @Override
    public String serializeList(List<?> list, CollectionFormat format) {
        return jacksonAdapter.serializeList(list, format);
    }

    public <T> T deserialize(String value, Type type) throws IOException {
        final Matcher matcher = MINIMUM_DATETIME_PATTERN.matcher(value);
        final String serializedString;

        // We have to replace matches because service returns a format that is not parsable from OffsetDateTime when
        // entities are created.
        if (matcher.find(0)) {
            logger.verbose("Found instances of '{}' to replace. Value: {}", MINIMUM_DATETIME_PATTERN.pattern(), value);
            serializedString = matcher.replaceAll(MINIMUM_DATETIME_FORMATTED);
        } else {
            serializedString = value;
        }

        return jacksonAdapter.deserialize(serializedString, type, SerializerEncoding.XML);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T deserialize(String value, Type type, SerializerEncoding encoding) throws IOException {
        if (encoding != SerializerEncoding.XML) {
            return jacksonAdapter.deserialize(value, type, encoding);
        }

        if (Object.class == type) {
            return (T) value;
        } else {
            return (T) deserialize(value, type);
        }
    }

    @Override
    public <T> T deserialize(HttpHeaders headers, Type type) throws IOException {
        return jacksonAdapter.deserialize(headers, type);
    }
}