2
1
0

ScriptRunnerから、外部APIをRESTで呼び出して必要な情報を取得したいと考えています。

以下のようなコードです。

//for endpoint
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript

//for RestAPI
import groovyx.net.http.RESTClient
import groovyx.net.http.HttpResponseException

//for json
import groovy.json.JsonOutput

// const
final String baseurl = "https://XXX.com/"
final String path = 'api/YYY'
final String auth = 'Basic ZZZ'

@BaseScript CustomEndpointDelegate delegate
def https = new RESTClient(baseurl);
try {
    https.post(
        path: path,
        headers: ['Accept':'application/json', 'Content-Type': 'application/json', 'Authorization':auth],
    ){
        res, json -> return JsonOutput.prettyPrint(JsonOutput.toJson(json));
    }
} catch(HttpResponseException e) {
    def r = e.response
    log.error("Success: $r.success");
    log.error("Status:  $r.status");
    log.error("Reason:  $r.statusLine.reasonPhrase");
    log.error("Content: \n${JsonOutput.prettyPrint(JsonOutput.toJson(r.data))}");
}

ScriptRunnerのコンソールにて動作を確認したところ、Post function failedとなりました。

JIRAを動かしているTomcatのcatalina.outを確認したところ、以下のようなログが出ているようでした。

07-Feb-2020 13:10:35.426 WARNING [ContainerBackgroundProcessor[StandardEngine[Catalina]]] org.apache.catalina.valves.StuckThreadDetectionValve.notifyStuckThreadDetected Thread [http-nio-8080-exec-13] (id=[31]) has been active for [122,152] milliseconds (since [2/7/20 1:08 PM]) to serve the same request for [http://xxxx/jira/rest/scriptrunner/latest/user/exec/] and may be stuck (configured threshold for this StuckThreadDetectionValve is [120] seconds). There is/are [3] thread(s) in total that are monitored by this Valve and may be stuck. 

処理時間がそんなにかかるとは思えないものの、Tomcatのserver.xmlからValveの時間を120秒から300秒に増やし、JIRAを再起動して再度試したところ、以下のようなエラーが今度はlocalhost.logに出るようになりました。

07-Feb-2020 20:26:30.436 SEVERE [http-nio-8080-exec-4] org.apache.catalina.core.StandardHostValve.custom Exception Processing ErrorPage[errorCode=500, location=/internal-error]
        java.lang.IllegalStateException: getOutputStream() has already been called for this response

JIRA ServiceDeskが動作している環境上、プロキシを介さないと外部にアクセスできないため、プロキシが悪さをしているのかとも考えましたが、JIRAの起動プロセスを確認したところ、プロキシは適切に設定されているようでした。JIRAのアドオンなども、ブラウザ上から取得できておりますので、その点は問題ないと考えています。

また、同様のREST EndpointにJIRAが動作している環境(CentOS 7.3)よりcURLでアクセスしたところ、期待した応答が返ってくるため、プロキシに問題があるわけでもなさそうです。


このとき、どのようなケースが考えられるでしょうか。。。


動作環境は、以下の通りです。

JIRA: v8.5.1

JIRA ServiceDesk: 4.5.1(Server版)

ScriptRunner: 5.6.13.1-p5

JIRAが動作している基盤: CentOS Linux release 7.3.1611 (Core) 

    Commentコメントを追加...

    2 回答

    1.  
      1
      0
      -1

      参考になるかわかりませんが、実際に動くものと組み合わせると下記でできました。

      import com.atlassian.jira.component.ComponentAccessor;
      import com.atlassian.jira.issue.Issue;
      import java.time.LocalDateTime;
      import java.time.format.DateTimeFormatter;
      import org.apache.log4j.Level;
      import org.apache.log4j.Logger;
      import groovy.json.JsonSlurper;
         
      def myLog = Logger.getLogger("com.onresolve.jira.groovy");
      myLog.setLevel(Level.DEBUG);
        
      String baseUrl = ComponentAccessor.getApplicationProperties().getString("jira.baseurl");
      LocalDateTime nowDate = LocalDateTime.now();
      DateTimeFormatter dtformat = DateTimeFormatter.ofPattern("yyyy-MM-dd");
      String dateFrom = "2019-04-01";
      String dateTo   = dtformat.format(nowDate);
         
      Issue issue = (Issue) issue;
      if (Objects.isNull(issue)) return;
        
      String tempoExpenceRestApiUrl = "/rest/tempo-core/1/expense?scopeType=ISSUE&scopeId=" + issue.getId() + "&dateFrom=" + dateFrom + "&dateTo=" + dateTo;
      String expenseUrl = baseUrl +  tempoExpenceRestApiUrl;
      boolean ConnectionDecisionFlug = false;
        
      HttpURLConnection expenseURLConnection = getURLConnection(expenseUrl);
       
      ConnectionDecisionFlug = getConnectionDecision(expenseURLConnection);
      //接続判定
      if(!ConnectionDecisionFlug) return;
        
      List<Map> expenseJsonParseTextList = getJsonParseTextList(expenseURLConnection);
        
      Map<Long, Long> expenseByscopeId_Map = new TreeMap<Long, Long>();
        
      Integer expenseTotal = getExpenseTotal( expenseJsonParseTextList, expenseByscopeId_Map);
        
      return expenseTotal;
        
      /*
      ----Function----
      */
        
      public HttpURLConnection getURLConnection(String restApiUrl){
          try{
              URL Url = new URL(restApiUrl);
              HttpURLConnection UrlConnection = (HttpURLConnection) Url.openConnection();
              UrlConnection.setRequestMethod("GET");
              UrlConnection.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString("userName:password".getBytes()));
              UrlConnection.setRequestProperty("Content-Type", "application/json");
              UrlConnection.setRequestProperty("Accept", "application/json");
              return UrlConnection;
          }catch(IOException e){
              e.getMessage();
              e.printStackTrace();
          }
      }
        
      public getConnectionDecision(HttpURLConnection URLConnection){
        try{
            if (URLConnection.getResponseCode() == HttpURLConnection.HTTP_OK){
              return true;
            }else{
              throw new RuntimeException("Could not establish connection to resource at requested URL.");
            }
        } catch (IOException e) {
            e.getMessage();
            e.printStackTrace();
        }
      }
         
      public List<Map> getJsonParseTextList(HttpURLConnection URLConnection){
        try{
            URLConnection.connect();
            def jsonParseText = getjsonParseText(URLConnection);
            if(Objects.nonNull(jsonParseText)) return jsonParseText;
        } catch (IOException e) {
            e.getMessage();
            e.printStackTrace();
        }finally{
            URLConnection.disconnect();
        }
      }
        
      public List<Map> getjsonParseText(HttpURLConnection URLConnection){
          InputStream ips = URLConnection.getInputStream();
          InputStreamReader isr = new InputStreamReader(ips);
          BufferedReader br = new BufferedReader(isr);
          try{
              String nextLine = br.readLine();
              List<Map> jsonParseTextList = (List<Map>) new JsonSlurper().parseText(nextLine);
              if(Objects.nonNull(nextLine)) return jsonParseTextList;
              else throw new RuntimeException("The value of nextLine is null.")
          }catch(IOException e){
              e.getMessage();
              e.printStackTrace();
          }finally{
              isr.close();
              br.close();
          }
      }
        
      public Integer getExpenseTotal( expenseJsonParseTextList, expenseByscopeId_Map){
        for (Object expence : expenseJsonParseTextList) {
          Map expenseMap = (Map) expence;
          Long scopeId = expenseMap.get('scope').get('scopeId');
          Long amount = expenseMap.get('amount');
          Long expense = amount;
              if (expenseByscopeId_Map.containsKey(scopeId)) {
                  expense = expense + expenseByscopeId_Map.get(scopeId);
              }
              expenseByscopeId_Map.put(scopeId, expense);
              log.warn("CHEAK \t" + expense+"\t"+scopeId);
          }
          Long expenseTotal = 0;
          //scopeIdと issue.getId()は同一値
          if(expenseByscopeId_Map.containsKey(issue.getId())){
          expenseTotal+= expenseByscopeId_Map.get(issue.getId());
      }
          return (Integer)expenseTotal;
      }

      ※補足
      質問の本質とは関係ないが、JavaScriptでjsonを取り扱う場合

      var baseUrl = AJS.params.baseURL;
      var issueKey = JIRA.Issue.getIssueKey();
      var jiraWorklogRestApiUrl = "/rest/api/2/issue/"
      var worklogRestApiUrl =  baseUrl + jiraWorklogRestApiUrl + issueKey + "/worklog/";
      var rateApiUrl   = "https://prudential.rickcloud.jp/jira/rest/tempo-accounts/1/ratetable/"
      var memberApiUrl = "https://prudential.rickcloud.jp/jira/rest/tempo-teams/2/team/10/member";
      var oReq = new XMLHttpRequest();
      var oReq1 = new XMLHttpRequest();
      var oReq2 = new XMLHttpRequest();
      oReq.open("GET", rateApiUrl);
      oReq.send();
       
      var rateMap = new Map();
      var memberMap = new Map();
      var roleIdMapByUserName_Map = new Map();
       
      oReq.onreadystatechange = function() {
        if (oReq.readyState == 4 && oReq.status == 200) { 
          var rateData = JSON.parse(oReq.responseText);
          rateMap = getRatesMap(rateData);
          //console.log(rateMap);
          oReq1.open("GET", memberApiUrl);
          oReq1.send()
        }
      }
       
      oReq1.onreadystatechange = function() {
        if (oReq1.readyState == 4 && oReq1.status == 200) {  
          var memberData = JSON.parse(oReq1.responseText);
          memberMap = getMemberMap(memberData)
          roleIdMapByUserName_Map = getRoleIdMapByUserName_Map(rateMap, memberMap);
          oReq2.open("GET", worklogRestApiUrl);
          oReq2.send()
        }
      }
       
      oReq2.onreadystatechange = function() {
        if (oReq2.readyState == 4 && oReq2.status == 200) {  
          var worklogJsonData = JSON.parse(oReq2.responseText);
          console.log(worklogJsonData);
          calculateActualLabor(worklogJsonData.worklogs , rateMap , roleIdMapByUserName_Map);
        }
      }
       
      function calculateActualLabor(worklogJsonData , rateMap , roleIdMapByUserName_Map){
        var total;
        console.log(worklogJsonData.length);
        for (var i = 0; i < worklogJsonData.length; i++){
          var workerName = worklogJsonData[i].author.name;
          var timeSpentSeconds = worklogJsonData[i].timeSpentSeconds;
          var hour = timeSpentSeconds / 3600;
          var mapIter = roleIdMapByUserName_Map.keys();
          if(mapIter.next().value == workerName){
            var map = roleIdMapByUserName_Map.get(workerName);
            var amount = 0;
            for (let [key, value] of map) {
              amount = map.get(key)
            }
            console.log(amount * hour);
          }
        }
      }
       
      function getworklogMap(worklogData){
        var workLogJsonData = [];
        for(var i = 0; i < worklogData.length; i++){
          workLogJsonData[i].push(worklogData[i].worklogs);
        }
        return workLogJsonData;
      }
       
      function getRoleIdMapByUserName_Map(rateMap, memberMap){
        var roleIdMapByUserName_Map = new Map();
        for (var [key, value] of memberMap) {
            var userName  = key, roleId  = value;
            var amount = rateMap.get(roleId);
            var amountByroleId_Map1 = new Map();
            amountByroleId_Map1.set(roleId, amount);
            roleIdMapByUserName_Map.set(userName, amountByroleId_Map1);
        }
        return roleIdMapByUserName_Map;
      }
       
      function getMemberMap(data){
        var memberMap = new Map();
        for(var i = 0, len = data.length; i < len; i++){
          var name = data[i].member.name;
          var roleId = data[i].membership.role.id;
          //console.log(name + "\t" + roleId);
          memberMap.set(name, roleId);
        }
        return memberMap;
      }
       
      function getRatesMap(data){
        var rateMap = new Map()
        for(var i = 0, len = data.length; i < len; i++){
          if(data[i].parent){
            var rates = data[i].parent.rates;
            for(var j = 0; j < rates.length; j++){
              var id = 0;
              if(rates[j].link.id == undefined){
                id = rates[j].link.id = -1;
              }else{
                id = rates[j].link.id;
              }
              var amount = rates[j].amount;
              rateMap.set(id,amount)
            }
          }
        }
        return rateMap;
      }
        Commentコメントを追加...
      1.  
        3
        2
        1

        コメントありがとうございます!参考になります。

        RestClientに対してsetProxyで改めてプロキシを設定したところ、想定した動作をすることを確認できました。

        http://javadox.com/org.codehaus.groovy.modules.http-builder/http-builder/0.6/groovyx/net/http/HTTPBuilder.html#setProxy(java.lang.String,%20int,%20java.lang.String)

        JIRAインスタンスの起動オプションに設定するプロキシとは別に、コード上からプロキシを利用して外部APIを呼び出す場合、明示的にしていしてあげなければいけないようです。。

        1. son

          無事動作してよかったです!

          そうなのですね。
          教えていただきありがとうございます!

        Commentコメントを追加...