Jython 使ってJavaからPython (1)

なぜか Jython の情報が少ない。Python ってそんなに他言語と一線を画すような拘りがあるのだろうか?
PythonPythonの良い所を利用して、JavaJavaの長所を活かして相互補助して使おうと考えないのだろうか?

JavaPython 実行で、パラメータ渡し、結果受け取りでハマったのが
・・・2バイト文字の結果を Java で受け取るケース
hello.py というテストの Python ソース・・・アホみたいなコードです

# -*- coding: UTF-8 -*-
def helloFunc(m):
    str = "Hello Python : " + m
    print(str)
    sts = "あ"
    res = "helloFunc result = " + sts
    return res.decode("UTF-8")
result = helloFunc(message)

1行目 # -*- coding: UTF-8 -*- を書いているから、Java からセットされた2バイト文字が
UTF-8 で扱われる?で正しいのか???
helloFunc() の戻り、return で返すものを unicode から UTF-8 にデコードする。

Java 呼び出し側

import org.python.core.PyObject;
import org.python.util.PythonInterpreter;
Properties props = new Properties();
props.put("python.console.encoding", "UTF-8");
PythonInterpreter.initialize(System.getProperties(), props, new String[]{});
try(PythonInterpreter py = new PythonInterpreter()){

   py.set("message", "あいう");
   py.execfile("C:/hello.py");

   PyObject pyResult = py.get("result");

   System.out.println("hello.py の 結果:" + pyResult.toString() );

}catch(Exception e){
   e.printStackTrace();
}

結果は、

Hello Python : あいう
hello.py の 結果:helloFunc result = あ

new PythonInterpreter() の実行が、とにかく遅い!!
PythonInterpreter インスタンスは一度生成したら、そのまま使い回しはできるようである。

パラメータ渡し方は他にも、こんな方法ある。
java - Adding arguments in Jython PythonInterpreter to the "execfile" function - Stack Overflow

JavaからPythonをcall 試したら、、

Java から Python を call するのに、Process 作ってシェル実行でなく、Jython を使うこと検討する。
シェル実行だと結果が欲しい時に実行結果の標準出力や標準エラー出力ハンドリングをしなきゃならないからだ。
Maven で、以下より jython-standalone JAR を取得

<dependency>
    <groupId>org.python</groupId>
    <artifactId>jython-standalone</artifactId>
    <version>2.7.1</version>
</dependency>

PythonInterpreter インスタンスを単純に、new PythonInterpreter() で生成して
execfile(String filename) で、実行する Python ソースファイルパスを
指定実行するが、動きはするが以下の警告メッセージが出た。

console: Failed to install '': java.nio.charset.UnsupportedCharsetException: cp0.

Jython のバグである。
http://bugs.jython.org/issue2222
http://8thstring.blogspot.com/2014/11/console-failed-to-install.html
JVM起動オプションに、-Dpython.console.encoding=UTF-8 付けるなんて
書いてあるけど、
そんな事するんじゃなくて!PythonInterpreter 生成前に、python.console.encoding 属性が UTF-8 であることを教えてやる。

Properties prop = new Properties();
prop.put("python.console.encoding", "UTF-8");
PythonInterpreter.initialize(System.getProperties(), prop, new String[]{});

try(PythonInterpreter py = new PythonInterpreter()){
     py.execfile("c:/var/test.py");
}

JSONのキーから、JsonElemnt 抽出を関数型インターフェースにする

JSONのキーから、JsonElement - Oboe吹きプログラマの黙示録
"." ドットで繋げたJSONのキーから JsonElement を抽出する
これを更に、関数型インターフェースにする。Reader 、JsonElement 、JSON文字列、各々に対するパターンを
書いたが、JsonElement に対するこのインスタンスは、PATH を変えながら再利用できるはずだ。

package org.yipuran.gsonhelper;

import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.stream.StreamSupport;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
 * JSONキーPATHによるJsonElement抽出.
 */
@FunctionalInterface
public interface JsonPathSearch extends Serializable{
   void accept(Optional<JsonElement> je);

   public static JsonPathSearch of(Consumer<Optional<JsonElement>> consumer){
      return e->consumer.accept(e);
   }
   default void compute(String key, JsonElement je){
      accept(elementParse(je, key));
   }
   default void compute(String key, Reader reader){
      accept(elementParse(new JsonParser().parse(reader), key));
   }
   default void compute(String key, String jsonstring){
      accept(elementParse(new JsonParser().parse(new StringReader(jsonstring==null ? "" : jsonstring)), key));
   }
   static Optional<JsonElement> elementParse(JsonElement je, String key){
      if (!je.isJsonObject()) return Optional.empty();
      if (key.contains(".")){
         String[] sp = key.split("\\.");
         if (sp.length > 1){
            return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
                  je.getAsJsonObject().entrySet().iterator(), Spliterator.ORDERED ), false)
               .filter(e->e.getKey().equals(sp[0])).findAny()
               .flatMap(e->elementParse(e.getValue(), key.replaceFirst("^[\\w_]+\\.", "")));
         }else{
            return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
                  je.getAsJsonObject().entrySet().iterator(), Spliterator.ORDERED ), false)
               .filter(e->e.getKey().equals(sp[0])).findAny().map(e->e.getValue());
         }
      }
      return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
            je.getAsJsonObject().entrySet().iterator(), Spliterator.ORDERED ), false)
         .filter(e->e.getKey().equals(key)).findAny().map(e->e.getValue());
   }
}

使用例、JSON ファイルの内容が以下の場合、

 { "a":{ "b":{ "c": 2345 , "num":[0,1,2] } } }
try(InputStream in = new FileInputStream("test.json");
     Reader reader = new InputStreamReader(in, "UTF-8")){
   JsonElement je = new JsonParser().parse(reader);
   JsonPathSearch js = JsonPathSearch.of(je->{
     je->je.ifPresent(e->{
         System.out.println( e.getAsInt() );
     });
   });
   js.compute("a.b.c", je);
}catch(IOException e){
  e.printStackTrace();
}

あるいは、"a.b.num" を指定して、取得する配列を JsonArray に変換して処理する。

JsonPathSearch.of(je->{
   je.ifPresent(e->{
      e.getAsJsonArray().forEach(t->{
         System.out.println( t.getAsInt() );
      });
   });
}).compute("a.b.num", reader);

JSONのキーから、JsonElement

先日、JSONのキーを、GSONを使用してセパレータ "." ドットで繋げて値を取得するものを書いてみた。
oboe2uran.hatenablog.com
でも、配列など JsonArray として取得して使用したい場合もあり要件が外れる時もある。
それに長ったらしいコードで気に要らない。
セパレータ "." ドットでキーを繋げて指定し、Optional<JsonElement > で取得したい。
キーは、半角英数字と "_"アンダースコア のみという約束で、以下のメソッドを作成

public static Optional<JsonElement> elementParse(Reader reader, String key){
   return elementParse(new JsonParser().parse(reader), key);
}
public static Optional<JsonElement> elementParse(JsonElement je, String key){
   if (!je.isJsonObject()) return Optional.empty();
   if (key.contains(".")){
      String[] sp = key.split("\\.");
      if (sp.length > 1){
         return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
               je.getAsJsonObject().entrySet().iterator(), Spliterator.ORDERED ), false)
            .filter(e->e.getKey().equals(sp[0])).findAny()
            .flatMap(e->elementParse(e.getValue() , key.replaceFirst("^[\\w_]+\\.", "")));
      }else{
         return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
               je.getAsJsonObject().entrySet().iterator(), Spliterator.ORDERED ), false)
            .filter(e->e.getKey().equals(sp[0])).findAny().map(e->e.getValue());
      }
   }
   return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
         je.getAsJsonObject().entrySet().iterator(), Spliterator.ORDERED ), false)
      .filter(e->e.getKey().equals(key)).findAny().map(e->e.getValue());
}

使い方

try(InputStream in = new FileInputStream("test.json");
    Reader reader = new InputStreamReader(in, "UTF-8")){
    Optional<JsonElement> e = elementParse(reader, "a.b.c");
    e.ifPresent(je->{
          // TODO
    });

とするか

try(InputStream in = new FileInputStream("test.json");
    Reader reader = new InputStreamReader(in, "UTF-8")){
    JsonElement elment = new JsonParser().parse(reader);
    Optional<JsonElement> e = elementParse(elment, "a.b.c");
    e.ifPresent(je->{
          // TODO
    });

SlackAPI 問い合わせを Java HTTPS通信して実行する

証明書認証をスキップするような空証明書での HTTPS 通信を Java で書いて、
サンプルを試す先=相手側は、なかなか見つからないものです。
Slack API というのが、HTTPS でした。これなら比較的自由に試せます。。
まずは、常套手段である誰もが以下のような X509証明を勝手にしたことにするクラスを用意します。

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;

public class NonAuthentication implements X509TrustManager{
   /* @see javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert.X509Certificate[], java.lang.String) */
   @Override
   public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException{
   }
   /* @see javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert.X509Certificate[], java.lang.String) */
   @Override
   public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException{
   }
   /* @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() */
   @Override
   public X509Certificate[] getAcceptedIssuers(){
      return null;
   }
}

これを使い、SSLSocketFactory からSSL通信します。
URL Slacack APIの途中までは、固定なはずなので、固定にしてます。
通信のタイムアウトを60秒にします。
今回は、プロキシを間に指定しません。
結果の受け取りは、文字列です。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * Slack APIへ問い合わせ、 application/x-www-form-urlencoded 
 */
public class SlackQuery{
   Logger logger = LoggerFactory.getLogger(this.getClass());
   private String token;

   public SlackQuery(String token){
      this.token = token;
   }
   public String query(String apiname, String method, Map<String, String> params) throws IOException{
      String result = null;
      try{
         SSLContext ctx = SSLContext.getInstance("SSL");
         ctx.init(null, new X509TrustManager[]{ new NonAuthentication() }, null);
         SSLSocketFactory factory = ctx.getSocketFactory();
         URL url = new URL("https://slack.com/api/" + apiname);
         HttpsURLConnection uc = (HttpsURLConnection)url.openConnection();
         uc.setSSLSocketFactory(factory);
         /* HTTPリクエストヘッダの設定 */
         uc.setDoOutput(true);                  // こちらからのデータ送信を可能とする
         uc.setReadTimeout(60 * 1000);             // 読み取りタイムアウト値
         uc.setRequestMethod(method);              // URL 要求メソッドを設定

         Map<String, String> parameters = new HashMap<>();
         parameters.put("token", token);
         params.entrySet().stream().forEach(e->{
            try{
               parameters.put(e.getKey(), URLEncoder.encode(e.getValue(), "UTF-8"));
            }catch(Exception ex){
               logger.warn(ex.getMessage(), ex);
            }
         });
         String sendstring = parameters.entrySet().stream().map(e->e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
         uc.setRequestProperty("Content-Length", Integer.toString(sendstring.getBytes("utf8").length));
         // コネクション確立→送信
         uc.connect();
         OutputStreamWriter osw = new OutputStreamWriter(uc.getOutputStream(), "utf8");
         osw.write(sendstring);
         osw.flush();
         int httpresponsecode = uc.getResponseCode();
         logger.debug("# HTTP response code : " + httpresponsecode );

         // 戻り値取得
         uc.getHeaderFields().entrySet().stream().forEach(e->{
            logger.debug("# "+ e.getKey() + " → " + e.getValue() );
         });
         // 応答読込
         try(BufferedReader in = new BufferedReader(new InputStreamReader(uc.getInputStream(), "utf8"))){
            StringBuilder out = new StringBuilder();
            char[] buf = new char[1024];
            int n;
            while((n = in.read(buf)) >= 0){
               out.append(buf, 0, n);
            }
            result = out.toString();
         }catch(IOException e){
            throw new RuntimeException(e);
         }
      }catch(NoSuchAlgorithmException | KeyManagementException| MalformedURLException ex){
         logger.error(ex.getMessage(), ex);
      }
      return result;
   }
}

Slack の管理ページから、問い合わせ対象のチームの トークンを取得しておきます。
取得したトークンで実行します

SlackQuery sq = new SlackQuery("xoxp-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXXXX");

String res = sq.query("groups.list", "GET",  new HashMap<>());

結果は、JSON文字列になります。

logback.xml に記述する appender のサンプル

同じような, logback.xml を書くことが多いので、サンプル

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE logback>
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
	<Target>System.out</Target>
	<encoder>
		<Pattern>%-23d{yyyy/MM/dd HH:mm:ss.SSS} %-5p [%thread] %m\t\t\t[%C{0}.%method:%line]%n</Pattern>
	</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
	<File>/var/log/dummy.log</File>
	<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
		<FileNamePattern>/var/log/dummy.%d{yyyy-MM-dd}.log</FileNamePattern>
		<maxHistory>12</maxHistory>
	</rollingPolicy>
	<encoder>
		<charset>UTF-8</charset>
		<Pattern>%-23d{yyyy/MM/dd HH:mm:ss.SSS} %-5p [%thread] %m\t\t\t[%C{0}.%method]%n</Pattern>
	</encoder>
</appender>

<logger name="org.yipuran">
   <level value="debug" />
   <appender-ref ref="STDOUT" />
	<appender-ref ref="FILE" />
</logger>

</configuration>

レイアウトの クラス名出力は、昔は、%C{0} でなくて %class を指定して
パッケージからのフルパスのクラス名で表示させたりしたけど、最近、そこまでの必要性が
なくなった。

Java8 system epoc time → LocalDateTime

今更だけど、長くてわすれるので、メモです

long milliseconds = System.currentTimeMillis();
LocalDateTime time = Instant.ofEpochMilli(milliseconds).atZone(ZoneId.systemDefault()).toLocalDateTime();
long seconds;
LocalDateTime time = Instant.ofEpochSecond(seconds).atZone(ZoneId.systemDefault()).toLocalDateTime();