All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.camel.component.servicenow.ServiceNowMetaDataExtension Maven / Gradle / Ivy

There is a newer version: 4.9.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.component.servicenow;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.camel.CamelContext;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.extension.MetaDataExtension;
import org.apache.camel.component.extension.metadata.AbstractMetaDataExtension;
import org.apache.camel.component.extension.metadata.MetaDataBuilder;
import org.apache.camel.component.servicenow.model.DictionaryEntry;
import org.apache.camel.support.PropertyBindingSupport;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.StringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An implementation of the MetaData extension {@link MetaDataExtension} that retrieve information about ServiceNow
 * objects as Json Schema as per draft-04 specs.
 */
final class ServiceNowMetaDataExtension extends AbstractMetaDataExtension {
    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceNowMetaDataExtension.class);
    private final ConcurrentMap properties;

    ServiceNowMetaDataExtension() {
        this(null);
    }

    ServiceNowMetaDataExtension(CamelContext context) {
        super(context);

        this.properties = new ConcurrentHashMap<>();
    }

    @Override
    public Optional meta(Map parameters) {
        final String objectType = (String) parameters.get("objectType");
        final String metaType = (String) parameters.getOrDefault("metaType", "definition");

        // Retrieve the table definition as json-scheme
        if (ObjectHelper.equalIgnoreCase(objectType, ServiceNowConstants.RESOURCE_TABLE)
                && ObjectHelper.equalIgnoreCase(metaType, "definition")) {
            final MetaContext context = new MetaContext(parameters);

            // validate meta parameters
            ObjectHelper.notNull(context.getObjectType(), "objectType");
            ObjectHelper.notNull(context.getObjectName(), "objectName");

            try {
                return tableDefinition(context);
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }

        // retrieve the list of tables excluding those used for import sets
        if (ObjectHelper.equalIgnoreCase(objectType, ServiceNowConstants.RESOURCE_TABLE)
                && ObjectHelper.equalIgnoreCase(metaType, "list")) {
            final MetaContext context = new MetaContext(parameters);

            // validate meta parameters
            ObjectHelper.notNull(context.getObjectType(), "objectType");

            try {
                return tableList(context);
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }

        // retrieve the list of import set tables
        if (ObjectHelper.equalIgnoreCase(objectType, ServiceNowConstants.RESOURCE_IMPORT)
                && ObjectHelper.equalIgnoreCase(metaType, "list")) {
            final MetaContext context = new MetaContext(parameters);

            // validate mate parameters
            ObjectHelper.notNull(context.getObjectType(), "objectType");

            try {
                return importSetList(context);
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }

        throw new UnsupportedOperationException("Unsupported object type <" + objectType + ">");
    }

    private Optional tableDefinition(MetaContext context) throws Exception {
        final List names = getObjectHierarchy(context);
        final ObjectNode root = context.getConfiguration().getOrCreateMapper().createObjectNode();
        final String baseUrn
                = (String) context.getParameters().getOrDefault("baseUrn", "org:apache:camel:component:servicenow");

        // Schema
        root.put("$schema", "http://json-schema.org/schema#");
        root.put("id", String.format("urn:jsonschema:%s:%s)", baseUrn, context.getObjectName()));
        root.put("type", "object");
        root.put("additionalProperties", false);

        // Schema sections
        root.putObject("properties");
        root.putArray("required");

        loadProperties(context);

        for (String name : names) {
            context.getStack().push(name);

            LOGGER.debug("Load dictionary <{}>", context.getStack());
            loadDictionary(context, name, root);
            context.getStack().pop();
        }

        final String dateFormat = properties.getOrDefault("glide.sys.date_format", "yyyy-MM-dd");
        final String timeFormat = properties.getOrDefault("glide.sys.time_format", "HH:mm:ss");

        return Optional.of(
                MetaDataBuilder.on(getCamelContext())
                        .withAttribute(MetaData.CONTENT_TYPE, "application/schema+json")
                        .withAttribute(MetaData.JAVA_TYPE, JsonNode.class)
                        .withAttribute("date.format", dateFormat)
                        .withAttribute("time.format", timeFormat)
                        .withAttribute("date-time.format", dateFormat + " " + timeFormat)
                        .withPayload(root)
                        .build());
    }

    private Optional importSetList(MetaContext context) throws Exception {
        Optional response = context.getClient().reset()
                .types(MediaType.APPLICATION_JSON_TYPE)
                .path("now")
                .path(context.getConfiguration().getApiVersion())
                .path("table")
                .path("sys_db_object")
                .query("sysparm_exclude_reference_link", "true")
                .query("sysparm_fields", "name%2Csys_id")
                .query("sysparm_query", "name=sys_import_set_row")
                .trasform(HttpMethod.GET, this::findResultNode);

        if (response.isPresent()) {
            final JsonNode node = response.get();
            final JsonNode sysId = node.findValue("sys_id");

            if (sysId == null) {
                throw new IllegalStateException("Unable to determine sys_id of sys_import_set_row");
            }

            response = context.getClient().reset()
                    .types(MediaType.APPLICATION_JSON_TYPE)
                    .path("now")
                    .path(context.getConfiguration().getApiVersion())
                    .path("table")
                    .path("sys_db_object")
                    .query("sysparm_exclude_reference_link", "true")
                    .query("sysparm_fields", "name%2Csys_name")
                    .queryF("sysparm_query", "super_class=%s", sysId.textValue())
                    .trasform(HttpMethod.GET, this::findResultNode);

            if (response.isPresent()) {
                final ObjectNode root = context.getConfiguration().getOrCreateMapper().createObjectNode();

                processResult(response.get(), n -> {
                    final JsonNode name = n.findValue("name");
                    final JsonNode label = n.findValue("sys_name");

                    if (name != null && label != null) {
                        root.put(name.textValue(), label.textValue());
                    }
                });

                return Optional.of(
                        MetaDataBuilder.on(getCamelContext())
                                .withAttribute(MetaData.CONTENT_TYPE, "application/json")
                                .withAttribute(MetaData.JAVA_TYPE, JsonNode.class)
                                .withPayload(root)
                                .build());
            }
        }

        return Optional.empty();
    }

    private Optional tableList(MetaContext context) throws Exception {
        Optional response = context.getClient().reset()
                .types(MediaType.APPLICATION_JSON_TYPE)
                .path("now")
                .path(context.getConfiguration().getApiVersion())
                .path("table")
                .path("sys_db_object")
                .query("sysparm_exclude_reference_link", "true")
                .query("sysparm_fields", "name%2Csys_id")
                .query("sysparm_query", "name=sys_import_set_row")
                .trasform(HttpMethod.GET, this::findResultNode);

        if (response.isPresent()) {
            final JsonNode node = response.get();
            final JsonNode sysId = node.findValue("sys_id");

            response = context.getClient().reset()
                    .types(MediaType.APPLICATION_JSON_TYPE)
                    .path("now")
                    .path(context.getConfiguration().getApiVersion())
                    .path("table")
                    .path("sys_db_object")
                    .query("sysparm_exclude_reference_link", "true")
                    .query("sysparm_fields", "name%2Csys_name%2Csuper_class")
                    .trasform(HttpMethod.GET, this::findResultNode);

            if (response.isPresent()) {
                final ObjectNode root = context.getConfiguration().getOrCreateMapper().createObjectNode();

                processResult(response.get(), n -> {
                    final JsonNode superClass = n.findValue("super_class");
                    final JsonNode name = n.findValue("name");
                    final JsonNode label = n.findValue("sys_name");

                    if (superClass != null) {
                        final String impId = sysId != null ? sysId.textValue() : null;
                        final String superId = superClass.textValue();

                        if (impId != null && superId != null && ObjectHelper.equal(impId, superId)) {
                            LOGGER.debug("skip table: name={}, label={} because it refers to an import set", name, label);
                            return;
                        }
                    }

                    if (name != null && label != null) {
                        String key = name.textValue();
                        String val = label.textValue();

                        if (ObjectHelper.isEmpty(val)) {
                            val = key;
                        }

                        root.put(key, val);
                    }
                });

                return Optional.of(
                        MetaDataBuilder.on(getCamelContext())
                                .withAttribute(MetaData.CONTENT_TYPE, "application/json")
                                .withAttribute(MetaData.JAVA_TYPE, JsonNode.class)
                                .withAttribute("Meta-Context", ServiceNowConstants.RESOURCE_IMPORT)
                                .withPayload(root)
                                .build());
            }
        }

        return Optional.empty();
    }

    // ********************************
    // Properties
    // ********************************

    private void loadProperties(MetaContext context) throws Exception {
        if (!properties.isEmpty()) {
            return;
        }

        String offset = "0";

        while (true) {
            Response response = context.getClient().reset()
                    .types(MediaType.APPLICATION_JSON_TYPE)
                    .path("now")
                    .path(context.getConfiguration().getApiVersion())
                    .path("table")
                    .path("sys_properties")
                    .query("sysparm_exclude_reference_link", "true")
                    .query("sysparm_fields", "name%2Cvalue")
                    .query("sysparm_offset", offset)
                    .query("sysparm_query", "name=glide.sys.date_format^ORname=glide.sys.time_format")
                    .invoke(HttpMethod.GET);

            findResultNode(response).ifPresent(node -> processResult(node, n -> {
                if (n.hasNonNull("name") && n.hasNonNull("value")) {
                    properties.putIfAbsent(
                            n.findValue("name").asText(),
                            n.findValue("value").asText());
                }
            }));

            Optional next = ServiceNowHelper.findOffset(response, ServiceNowConstants.LINK_NEXT);
            if (next.isPresent()) {
                offset = next.get();
            } else {
                break;
            }
        }
    }

    // ********************************
    // Dictionary
    // ********************************

    private void loadDictionary(MetaContext context, String name, ObjectNode root) throws Exception {
        String offset = "0";

        while (true) {
            Response response = context.getClient().reset()
                    .types(MediaType.APPLICATION_JSON_TYPE)
                    .path("now")
                    .path(context.getConfiguration().getApiVersion())
                    .path("table")
                    .path("sys_dictionary")
                    .query("sysparm_display_value", "false")
                    .queryF("sysparm_query", "name=%s", name)
                    .query("sysparm_offset", offset)
                    .invoke(HttpMethod.GET);

            findResultNode(response).ifPresent(node -> processResult(node, n -> {
                processDictionaryNode(context, root, n);
            }));

            Optional next = ServiceNowHelper.findOffset(response, ServiceNowConstants.LINK_NEXT);
            if (next.isPresent()) {
                offset = next.get();
            } else {
                break;
            }
        }
    }

    private void processDictionaryNode(MetaContext context, ObjectNode root, JsonNode node) {
        if (node.hasNonNull("element")) {
            final String id = node.get("element").asText();

            if (ObjectHelper.isNotEmpty(id)) {
                String includeKey = "object." + context.getObjectName() + ".fields";
                String excludeKey = "object." + context.getObjectName() + ".fields.exclude.pattern";
                String fields = (String) context.getParameters().get(includeKey);
                String exclude = (String) context.getParameters().get(excludeKey);

                boolean included = true;

                if (ObjectHelper.isNotEmpty(fields) && ObjectHelper.isNotEmpty(exclude)) {
                    boolean isIncluded = Stream.of(fields.split(",")).map(StringHelper::trimToNull).filter(Objects::nonNull)
                            .anyMatch(id::equalsIgnoreCase);
                    boolean isExcluded = Pattern.compile(exclude).matcher(id).matches();

                    // if both include/exclude list is provided check if the
                    // fields ie either explicit included or not excluded.
                    //
                    // This is useful if you want to exclude all the i.e. sys_
                    // fields but want some i.e. the sys_id to be included
                    included = isIncluded || !isExcluded;
                } else if (ObjectHelper.isNotEmpty(fields)) {
                    // Only include fields that are explicit included
                    included = Stream.of(fields.split(",")).map(StringHelper::trimToNull).filter(Objects::nonNull)
                            .anyMatch(id::equalsIgnoreCase);
                } else if (ObjectHelper.isNotEmpty(exclude)) {
                    // Only include fields non excluded
                    included = !Pattern.compile(exclude).matcher(id).matches();
                }

                if (!included) {
                    return;
                }

                context.getStack().push(id);
                LOGGER.debug("Load dictionary element <{}>", context.getStack());

                try {
                    final DictionaryEntry entry
                            = context.getConfiguration().getOrCreateMapper().treeToValue(node, DictionaryEntry.class);
                    final ObjectNode property = ((ObjectNode) root.get("properties")).putObject(id);

                    // Add custom fields for code generation, json schema
                    // validators are not supposed to use this extensions.
                    final ObjectNode servicenow = property.putObject("servicenow");

                    // the internal type
                    servicenow.put("internal_type", entry.getInternalType().getValue());

                    switch (entry.getInternalType().getValue()) {
                        case "integer":
                            property.put("type", "integer");
                            break;
                        case "float":
                            property.put("type", "number");
                            break;
                        case "boolean":
                            property.put("type", "boolean");
                            break;
                        case "guid":
                        case "GUID":
                            property.put("type", "string");
                            property.put("pattern", "^[a-fA-F0-9]{32}");
                            break;
                        case "glide_date":
                            property.put("type", "string");
                            property.put("format", "date");
                            break;
                        case "due_date":
                        case "glide_date_time":
                        case "glide_time":
                        case "glide_duration":
                            property.put("type", "string");
                            property.put("format", "date-time");
                            break;
                        case "reference":
                            property.put("type", "string");
                            property.put("pattern", "^[a-fA-F0-9]{32}");

                            if (entry.getReference().getValue() != null) {
                                // the referenced object type
                                servicenow.put("sys_db_object", entry.getReference().getValue());
                            }

                            break;
                        default:
                            property.put("type", "string");

                            if (entry.getMaxLength() != null) {
                                property.put("maxLength", entry.getMaxLength());
                            }
                            break;
                    }

                    if (entry.isMandatory()) {
                        ArrayNode required = (ArrayNode) root.get("required");
                        if (required == null) {
                            required = root.putArray("required");
                        }

                        required.add(id);
                    }

                } catch (JsonProcessingException e) {
                    throw new RuntimeCamelException(e);
                } finally {
                    context.getStack().pop();
                }
            }
        }
    }

    // *************************************
    // Helpers
    // *************************************

    /**
     * Determine the hierarchy of a table by inspecting the super_class attribute.
     */
    private List getObjectHierarchy(MetaContext context) throws Exception {
        List hierarchy = new ArrayList<>();
        String query = String.format("name=%s", context.getObjectName());

        while (true) {
            Optional response = context.getClient().reset()
                    .types(MediaType.APPLICATION_JSON_TYPE)
                    .path("now")
                    .path(context.getConfiguration().getApiVersion())
                    .path("table")
                    .path("sys_db_object")
                    .query("sysparm_exclude_reference_link", "true")
                    .query("sysparm_fields", "name%2Csuper_class")
                    .query("sysparm_query", query)
                    .trasform(HttpMethod.GET, this::findResultNode);

            if (response.isPresent()) {
                final JsonNode node = response.get();
                final JsonNode nameNode = node.findValue("name");
                final JsonNode classNode = node.findValue("super_class");

                if (nameNode != null && classNode != null) {
                    query = String.format("sys_id=%s", classNode.textValue());
                    hierarchy.add(0, nameNode.textValue());
                } else {
                    break;
                }
            } else {
                break;
            }
        }

        return hierarchy;
    }

    private void processResult(JsonNode node, Consumer consumer) {
        if (node.isArray()) {
            Iterator it = node.elements();
            while (it.hasNext()) {
                consumer.accept(it.next());
            }
        } else {
            consumer.accept(node);
        }
    }

    private Optional findResultNode(Response response) {
        if (ObjectHelper.isNotEmpty(response.getHeaderString(HttpHeaders.CONTENT_TYPE))) {
            JsonNode root = response.readEntity(JsonNode.class);
            if (root != null) {
                Iterator> fields = root.fields();
                while (fields.hasNext()) {
                    final Map.Entry entry = fields.next();
                    final String key = entry.getKey();
                    final JsonNode node = entry.getValue();

                    if (ObjectHelper.equal("result", key, true)) {
                        return Optional.of(node);
                    }
                }
            }
        }

        return Optional.empty();
    }

    // *********************************
    // Context class
    // *********************************

    private final class MetaContext {
        private final Map parameters;
        private final ServiceNowConfiguration configuration;
        private final ServiceNowClient client;
        private final String instanceName;
        private final String objectName;
        private final String objectType;
        private final Deque stack;

        MetaContext(Map parameters) {
            this.parameters = parameters;
            this.configuration = new ServiceNowConfiguration();
            this.stack = new ArrayDeque<>();

            try {
                PropertyBindingSupport.bindProperties(getCamelContext(), configuration, new HashMap<>(parameters));
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }

            this.instanceName = (String) parameters.get("instanceName");
            this.objectType = (String) parameters.getOrDefault("objectType", ServiceNowConstants.RESOURCE_TABLE);
            this.objectName = (String) parameters.getOrDefault("objectName", configuration.getTable());

            ObjectHelper.notNull(instanceName, "instanceName");

            // Configure Api and OAuthToken ULRs using instanceName
            if (!configuration.hasApiUrl()) {
                configuration.setApiUrl(String.format("https://%s.service-now.com/api", instanceName));
            }
            if (!configuration.hasOauthTokenUrl()) {
                configuration.setOauthTokenUrl(String.format("https://%s.service-now.com/oauth_token.do", instanceName));
            }

            this.client = new ServiceNowClient(getCamelContext(), configuration);
        }

        public Map getParameters() {
            return parameters;
        }

        public ServiceNowConfiguration getConfiguration() {
            return configuration;
        }

        public ServiceNowClient getClient() {
            return client;
        }

        public String getInstanceName() {
            return instanceName;
        }

        public String getObjectType() {
            return objectType;
        }

        public String getObjectName() {
            return objectName;
        }

        public Deque getStack() {
            return stack;
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy