blob: 9912442210dd8d1953be88a1f25479294945aee8 [file] [log] [blame]
/**
* 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 javax.ws.rs.HttpMethod;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.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.util.IntrospectionSupport;
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<String, String> properties;
ServiceNowMetaDataExtension() {
this(null);
}
ServiceNowMetaDataExtension(CamelContext context) {
super(context);
this.properties = new ConcurrentHashMap<>();
}
@Override
public Optional<MetaData> meta(Map<String, Object> 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<MetaData> tableDefinition(MetaContext context) throws Exception {
final List<String> 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<MetaData> importSetList(MetaContext context) throws Exception {
Optional<JsonNode> 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<MetaData> tableList(MetaContext context) throws Exception {
Optional<JsonNode> 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<String> 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<String> 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<String> getObjectHierarchy(MetaContext context) throws Exception {
List<String> hierarchy = new ArrayList<>();
String query = String.format("name=%s", context.getObjectName());
while (true) {
Optional<JsonNode> 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<JsonNode> consumer) {
if (node.isArray()) {
Iterator<JsonNode> it = node.elements();
while (it.hasNext()) {
consumer.accept(it.next());
}
} else {
consumer.accept(node);
}
}
private Optional<JsonNode> findResultNode(Response response) {
if (ObjectHelper.isNotEmpty(response.getHeaderString(HttpHeaders.CONTENT_TYPE))) {
JsonNode root = response.readEntity(JsonNode.class);
if (root != null) {
Iterator<Map.Entry<String, JsonNode>> fields = root.fields();
while (fields.hasNext()) {
final Map.Entry<String, JsonNode> 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<String, Object> parameters;
private final ServiceNowConfiguration configuration;
private final ServiceNowClient client;
private final String instanceName;
private final String objectName;
private final String objectType;
private final Deque<String> stack;
MetaContext(Map<String, Object> parameters) {
this.parameters = parameters;
this.configuration = new ServiceNowConfiguration();
this.stack = new ArrayDeque<>();
try {
IntrospectionSupport.setProperties(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<String, Object> 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<String> getStack() {
return stack;
}
}
}