com.vmware.connectors.servicenow.ServiceNowController Maven / Gradle / Ivy
/*
* Copyright © 2017 VMware, Inc. All Rights Reserved.
* SPDX-License-Identifier: BSD-2-Clause
*/
package com.vmware.connectors.servicenow;
import com.google.common.collect.ImmutableMap;
import com.vmware.connectors.common.payloads.request.CardRequest;
import com.vmware.connectors.common.payloads.response.Card;
import com.vmware.connectors.common.payloads.response.CardAction;
import com.vmware.connectors.common.payloads.response.CardActionInputField;
import com.vmware.connectors.common.payloads.response.CardActionKey;
import com.vmware.connectors.common.payloads.response.CardBody;
import com.vmware.connectors.common.payloads.response.CardBodyField;
import com.vmware.connectors.common.payloads.response.CardBodyFieldType;
import com.vmware.connectors.common.payloads.response.Cards;
import com.vmware.connectors.common.utils.CardTextAccessor;
import com.vmware.connectors.common.JsonDocument;
import com.vmware.connectors.common.utils.Async;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.AsyncRestOperations;
import org.springframework.web.util.UriComponentsBuilder;
import rx.Observable;
import rx.Single;
import javax.validation.Valid;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
public class ServiceNowController {
private final static Logger logger = LoggerFactory.getLogger(ServiceNowController.class);
private static final String AUTH_HEADER = "x-servicenow-authorization";
private static final String BASE_URL_HEADER = "x-servicenow-base-url";
private static final String ROUTING_PREFIX = "x-routing-prefix";
private static final String REASON_PARAM_KEY = "reason";
/**
* The query param to specify which fields you want to come back in your
* ServiceNow REST calls.
*/
private static final String SNOW_SYS_PARAM_FIELDS = "sysparm_fields";
/**
* The query param to specify a limit of the results coming back in your
* ServiceNow REST calls.
*
* The default is 10,000.
*/
private static final String SNOW_SYS_PARAM_LIMIT = "sysparm_limit";
/**
* The maximum approval requests to fetch from ServiceNow. Since we have
* to filter results out based on the ticket_id param passed in by the
* client, this has to be sufficiently large to not lose results.
*
* I wasn't able to find a REST call that would allow me to bulk lookup the
* approval requests (or requests) by multiple request numbers
* (ex. REQ0010001,REQ0010002,REQ0010003), so I'm forced to do things a
* little less ideal than I would like (calling 1x per result of the
* sysapproval_approver call to be able to match it to the request numbers
* passed in by the client).
*/
private static final int MAX_APPROVAL_RESULTS = 10000;
private final AsyncRestOperations rest;
private final CardTextAccessor cardTextAccessor;
@Autowired
public ServiceNowController(
AsyncRestOperations rest,
CardTextAccessor cardTextAccessor
) {
this.rest = rest;
this.cardTextAccessor = cardTextAccessor;
}
@PostMapping(
path = "/cards/requests",
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
public Single> getCards(
@RequestHeader(AUTH_HEADER) String auth,
@RequestHeader(BASE_URL_HEADER) String baseUrl,
@RequestHeader(ROUTING_PREFIX) String routingPrefix,
@Valid @RequestBody CardRequest request
) {
logger.trace("getCards called, baseUrl={}, routingPrefix={}, request={}", baseUrl, routingPrefix, request);
Set requestNumbers = request.getTokens("ticket_id");
if (CollectionUtils.isEmpty(requestNumbers)) {
return Single.just(ResponseEntity.ok(new Cards()));
}
String email = request.getTokenSingleValue("email");
if (email == null) {
return Single.just(ResponseEntity.ok(new Cards()));
}
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", auth);
HttpEntity httpHeaders = new HttpEntity<>(headers);
return callForUserSysId(baseUrl, email, httpHeaders)
.flatMap(userSysId -> callForApprovalRequests(baseUrl, httpHeaders, userSysId))
.flatMapObservable(approvalRequests -> callForAllRequestNumbers(baseUrl, httpHeaders, approvalRequests))
.filter(info -> requestNumbers.contains(info.getNumber()))
.reduce(
new Cards(),
(cards, info) -> appendCard(cards, info, routingPrefix)
)
.toSingle()
.map(ResponseEntity::ok);
}
private Single callForUserSysId(
String baseUrl,
String email,
HttpEntity headers
) {
logger.trace("callForUserSysId called: baseUrl={}", baseUrl);
ListenableFuture> response = rest.exchange(
UriComponentsBuilder.fromHttpUrl(baseUrl + "/api/now/table/{userTableName}")
.queryParam(SNOW_SYS_PARAM_FIELDS, joinFields(SysUser.Fields.SYS_ID))
.queryParam(SNOW_SYS_PARAM_LIMIT, 1)
/*
* TODO - This is flawed. It turns out that emails do
* not have to uniquely identify users in ServiceNow.
* I am able to create 2 different sys_user records
* that have the same email.
*/
.queryParam(SysUser.Fields.EMAIL.toString(), email)
.buildAndExpand(
ImmutableMap.of(
"userTableName", SysUser.TABLE_NAME
)
)
.encode()
.toUri(),
HttpMethod.GET,
headers,
JsonDocument.class
);
return Async.toSingle(response)
.map(userInfoResponse -> userInfoResponse.getBody().read("$.result[0]." + SysUser.Fields.SYS_ID));
}
private Single> callForApprovalRequests(
String baseUrl,
HttpEntity headers,
String userSysId
) {
logger.trace("callForApprovalRequests called: baseUrl={}, userSysId={}", baseUrl, userSysId);
String fields = joinFields(
SysApprovalApprover.Fields.SYS_ID,
SysApprovalApprover.Fields.SYSAPPROVAL,
SysApprovalApprover.Fields.COMMENTS,
SysApprovalApprover.Fields.DUE_DATE,
SysApprovalApprover.Fields.SYS_CREATED_BY
);
ListenableFuture> response = rest.exchange(
UriComponentsBuilder.fromHttpUrl(baseUrl + "/api/now/table/{apTableName}")
.queryParam(SNOW_SYS_PARAM_FIELDS, fields)
.queryParam(SNOW_SYS_PARAM_LIMIT, MAX_APPROVAL_RESULTS)
.queryParam(SysApprovalApprover.Fields.SOURCE_TABLE.toString(), ScRequest.TABLE_NAME)
.queryParam(SysApprovalApprover.Fields.STATE.toString(), SysApprovalApprover.States.REQUESTED)
.queryParam(SysApprovalApprover.Fields.APPROVER.toString(), userSysId)
.buildAndExpand(
ImmutableMap.of(
"apTableName", SysApprovalApprover.TABLE_NAME
)
)
.encode()
.toUri(),
HttpMethod.GET,
headers,
JsonDocument.class
);
return Async.toSingle(response)
/*
* I had trouble getting JsonPath to return me something more meaningful than a List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy