mike-neckのブログ

Java or Groovy or Swift or Golang

Azure 上の Ubuntu で Swift 4.2.1 を動かす

f:id:mike_neck:20180828214340p:plain

この3連休で試したのでメモ。

Azure で立てた Linux VM(Ubuntu16.04) にて、 swift の docker イメージにある通りの手順で Swift4.2.1 をインストールしたところ、エラーが出てしまって Swift アプリケーションを動かせなかった。

具体的には

  • libdispatch-dev
  • libcurl4-openssl-dev
  • libicu-dev
  • libssl-dev
  • pkg-config

をインストールしてサンプルのアプリケーションに対してどの shared ライブラリーが使われるか確認したところ次のようなエラーが出てきた。

$ ldd ExampleApp 
./ExampleResponseApp: /usr/lib/x86_64-linux-gnu/libcurl.so.4: version `CURL_OPENSSL_3' not found (required by /usr/lib/swift/linux/libFoundation.so)
    linux-vdso.so.1 (0x00007ffe2db2a000)
    libFoundation.so => /usr/lib/swift/linux/libFoundation.so (0x00007fb764794000)
    libswiftCore.so => /usr/lib/swift/linux/libswiftCore.so (0x00007fb764279000)
    libswiftGlibc.so => /usr/lib/swift/linux/libswiftGlibc.so (0x00007fb76508b000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb76405a000)
    libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007fb763e57000)
# 以下略

調べてみると、 GitHub にある curl の issue が見つかった。

github.com

問題そのものは Ubuntu だか Debian だか、 Swift の問題っぽい。なので、 curl ではどうにも対処できない問題の様子だが、最後のワークアラウンドを適用すると動かすことができた。


以下、 Swift4.2.1 を Azure の Ubuntu で動かすために必要なスクリプト

#!/usr/bin/env bash

sudo apt-get -q update
sudo apt-get -y install libdispatch-dev libcurl4-openssl-dev libicu-dev libssl-dev pkg-config libgconf-2-4
sudo echo "deb http://security.ubuntu.com/ubuntu xenial-security main" >> /etc/apt/sources.list
sudo apt-get -y install libcurl3

curl -fSsL http://security.ubuntu.com/ubuntu/pool/main/i/icu/libicu55_55.1-7_amd64.deb -o libicu55.deb
sudo dpkg -i libicu55.deb
rm libicu55.deb

export SWIFT_VERSION=swift-4.2.1-RELEASE
export SWIFT_BRANCH=swift-4.2.1-release
export SWIFT_PLATFORM=ubuntu16.04

export SWIFT_URL=https://swift.org/builds/${SWIFT_BRANCH}/$(echo "$SWIFT_PLATFORM" | tr -d .)/${SWIFT_VERSION}/${SWIFT_VERSION}-${SWIFT_PLATFORM}.tar.gz

curl -fSsL ${SWIFT_URL} -o swift.tgz

sudo tar -xzf swift.tgz --directory / --strip-components=1
rm swift.tgz

sudo chmod -R o+r /usr/lib/swift

今読み返したけど、 Azure ほとんど関係なかった

Golang で XML をパースするために xsd から struct を作り出す

Go言語で XML をパースするメモ

f:id:mike_neck:20181121224358p:plain

とあるxmlgolangでパースしてデータを操作したいので、golangでxsdからstruct のコードを生成するツールを探したところ、次のようなのがあった

github.com

ところが、 リポジトリーの冒頭にも書いてあるように Stale since 2013 (= 2013 から古い)(stale には古い/新鮮ではないといった意味がある様子)とあり、最終更新が8ヶ月前なので、利用するのが少しためらわれた。

そこで、もう少し探してみたところ、次のようなライブラリーを見つけた。

github.com

更新も1ヶ月前なので多分大丈夫だろうということで、早速使ってみることにした。


インストール

go get aqwari.net/xml/...

コード生成

まず手元にパースしたいxmlのxsdを準備する

curl https://maven.apache.org/xsd/maven-4.0.0.xsd -o maven-4.0.0.xsd

次に xsdgen コマンドを呼び出す

xsdgen -o pom.go -pkg pom maven-4.0.0.xsd

なお、主なオプションは次の通り

$ xsdgen --help
Usage of xsdgen:
  -ns value
        target namespace(s) to generate types for
  -o string
        name of the output file (default "xsdgen_output.go")
  -pkg string
        name of the the generated package
  -r value
        replacement rule 'regex -> repl' (can be used multiple times)
  -v    print verbose output
  -vv
        print debug output

コードを生成すると次のようなgoコードが生成される

package pom

import "encoding/xml"

// 4.0.0+
//
// The conditions within the build runtime environment which will trigger the
// automatic inclusion of the build profile. Multiple conditions can be defined, which must
// be all satisfied to activate the profile.
type Activation struct {
    ActiveByDefault bool               `xml:"http://maven.apache.org/POM/4.0.0 activeByDefault,omitempty"`
    Jdk             string             `xml:"http://maven.apache.org/POM/4.0.0 jdk,omitempty"`
    Os              ActivationOS       `xml:"http://maven.apache.org/POM/4.0.0 os,omitempty"`
    Property        ActivationProperty `xml:"http://maven.apache.org/POM/4.0.0 property,omitempty"`
    File            ActivationFile     `xml:"http://maven.apache.org/POM/4.0.0 file,omitempty"`
}

func (t *Activation) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    type T Activation
    var overlay struct {
        *T
        ActiveByDefault *bool `xml:"http://maven.apache.org/POM/4.0.0 activeByDefault,omitempty"`
    }
    overlay.T = (*T)(t)
    overlay.ActiveByDefault = (*bool)(&overlay.T.ActiveByDefault)
    return d.DecodeElement(&overlay, &start)
}

パースする

では生成されたコードを使って適当なxmlをパースする

とりあえず、ここでは junit-jupiter-api-5.0.0-M3.pom をパースしてみる

まず main 関数. ここは何もしていない

package main

import (
    "encoding/xml"
    "io"
    "log"
    "os"
    "reflect"
)

func main() {
    err := run("cmd/go-xml-analyze/testdata/junit-jupiter-api-5.0.0-M3.pom")
    if err != nil {
        log.Fatalln("error", err)
    }
}

run 関数は普通の xml の取り回し

func run(file string) error {
    log.Println("parse file", file)
    pomXml, err := os.Open(file)
    if err != nil {
        log.Println("failed to open pom file.", err)
        return err
    }
    defer pomXml.Close()

    decoder := xml.NewDecoder(pomXml)

    for {
        elem, err := read(decoder)
        if err != nil {
            return err
        }
        if elem.show() {
            return nil
        }
    }
}

どうハンドリングすればよいかよくわからなかったので適当に作った struct たち

// ただハンドルするだけ
// 終わったら true/終わってなければ false
type Elem interface {
    show() bool
}

// pom の project 要素のハンドル
// dependencies の dependency 要素を表示する
func (m pom.Model) show() bool {
    for _, dep := range m.Dependencies.Dependency {
        log.Println("group:", dep.GroupId, "artifact:", dep.ArtifactId, "version:", dep.Version, "scope:", dep.Scope)
    }
    return true
}

// project 要素が出る前のハンドル
type Another struct {
    token xml.Token
}

func (a *Another) show() bool {
    return false
}

読み取りの処理

func read(decoder *xml.Decoder) (Elem, error) {
    token, err := decoder.Token()
    if err != nil {
        log.Println("failed to get token", err)
        return nil, err
    }
    switch tokenType := token.(type) {
    case xml.StartElement:
        var project Model
        err := project.UnmarshalXML(decoder, tokenType)
        if err != nil {
            log.Println("failed to parse pom", err)
            return nil, err
        }
        return project, nil
    default:
        return &Another{token: tokenType}, nil
    }
}

ポイントとしては、 一番上の要素を宣言して、 xml.Decoderxml.StartElement をその Unmarshal メソッドに渡すとデータをすべてパースしてくれるらしい

というわけで実行結果は次の通り.

2018/11/21 22:39:33 group: org.opentest4j artifact: opentest4j version: 1.0.0-M1 scope: compile
2018/11/21 22:39:33 group: org.junit.platform artifact: junit-platform-commons version: 1.0.0-M3 scope: compile

必要そうなデータが読み取れたようだ

Stream の実行順

今更なテーマだが、たまに忘れてしまうのでメモ


次のようなインターフェースとクラスがあるものとする。

RequestValidation

ある要求 T を受け付けるか拒否するか判定するインターフェース。判定した結果リクエストを受け付けられない場合は R を返す。リクエストを受け付けられる場合は empty になる。

interface RequestValidation<T, R> {
  Optional<R> test(T input);
}

UserPropertyUpdateRequest

あるシステムのユーザーの属性を更新する要求。

class UserPropertyUpdateRequest {
  @Nullable final String firstName;
  @Nullable final String lastName;
  @Nullable final String timeZone;
  // コンストラクターは省略
}

UserPropertyUpdateFailure

ユーザーの属性更新が受け付けられないというチェック結果。

class UserPropertyUpdateFailure {
  @NotNull final String field;
  @NotNull final String message;
  // コンストラクターは省略
}

このときに、次のようなユーザーの属性を更新するための値の仕様があるものとする。

  • firstName に空文字も空白も認められない
  • lastName に空文字も空白も認められない
  • timeZonejava.time.ZoneId で解決できるもののみ登録する
  • firstName / lastName / timeZone とも null の場合はそれぞれの値を更新しない

それぞれの仕様を表す RequestValidation を作ると次のような感じになる。

RequestValidation<UserPropertyUpdateRequest, UserPropertyUpdateFailure> firstNameValidation = req -> {
  String fn = req.firstName;
  if (fn == null) return Optional.empty();
  if (fn.isEmpty() || fn.isBlank()) return Optional.of(new UserPropertyUpdateFailure("firstName", "blank or empty"));
  return Optional.empty();
};
RequestValidation<UserPropertyUpdateRequest, UserPropertyUpdateFailure> lastNameValidation = req -> {
  String ln = req.lastName;
  if (ln == null) return Optional.empty();
  if (ln.isEmpty() || ln.isBlank()) return Optional.of(new UserPropertyUpdateFailure("lastName", "blank or empty"));
  return Optional.empty();
};
RequestValidation<UserPropertyUpdateRequest, UserPropertyUpdateFailure> timeZoneValidation = req -> {
  String tz = req.timeZone;
  if (tz == null) return Optional.empty();
  try {
    ZoneId.of(tz);
    return Optional.empty();
  } catch (Exception e) {
    return Optional.of(new UserPropertyUpdateFailure("timeZone", "unknown timezone"));
  }
};

これらのチェックを行う際に、一つだけ見つかったら他のチェックはしないような処理を書きたいとする。そこで、最初は次のように書くのだが…

Optional<UserPropertyUpdateRequest> result1 = firstNameValidation.test(request);
if (result1.isPresent()) {
  return someResultValue;
}
Optional<UserPropertyUpdateRequest> result2 = lastNameValidation.test(request);
if (result2.isPresent()) {
  return someResultValue;
}
//...

このように書くと、せっかくインターフェースを導入した意味がなくなる。そこで、 Stream を使う。

Optional<UserPropertyUpdateFailure> result = Stream.of(firstNameValidation, lastNameValidation, timeZoneValidation)
  .map(validation -> validation.test(request))
  .filter(Optional::isPresent)
  .map(Optional::get)
  .findFirst();

さて、この Stream は本当に最初の一つが見つかったら処理を終了してくれるだろうか…という不安もあって、 Stream の処理順が気になってしまった。

なお、 Stream はすべての処理を一つにまとめた処理をつくって、それを一つ一つの要素に対して最後適用していく。したがって findFirst で最初に見つかったものがあれば、それ以降の要素に対しては処理を行わない。つまり、一つだけ見つかったら他のチェックはしないように処理を書くという最初の目的は達成されている。

確認

念のために確認する。

次のようなメソッドを定義して、 Stream を作る。

RequestValidation<UserPropertyUpdateRequest, UserPropertyUpdateFailure> wrapLog(
    String name,
    RequestValidation<UserPropertyUpdateRequest, UserPropertyUpdateFailure> delegate) {
  return req -> {
    System.out.println(name);
    return delegate.test(req);
  };
}

そして、各 RequestValidation をラップして実行してみる。

Optional<UserPropertyUpdateFailure> result = Stream.of(
    wrapLog("firstName", firstNameValidation),
    wrapLog("lastName", lastNameValidation),
    wrapLog("timeZone", timeZoneValidation)
  ).map(validation -> validation.test(request))
  .filter(Optional::isPresent)
  .map(Optional::get)
  .findFirst();

実行結果は次のとおり。

f:id:mike_neck:20181113024951p:plain

RSocket の request-stream のサーバー/クライアントを作る

RSocket で request-stream のサーバーおよびクライアントを組み立てるときのコード例

f:id:mike_neck:20181102013529p:plain


サーバー

サーバーの構築

final Disposable server = RSocketFactory.receive()
    .acceptor(new MySocketAcceptor())
    .transport(TcpServerTransport.create("localhost", 7000))
    .start()
    .subscribe();

SocketAcceptor

request-stream をみたすサーバーの SocketAcceptor が返す RSocketrequestStream(Payload) を返す

以下の例は、 クライアントの requestStream にて送られてきた Payload をそのまま何度も返すサーバーのもの。

class MySocketAcceptor implements SocketAcceptor {
  @Override
  public Mono<RSocket> accept(ConnectionSetupPayload setup, RSocket sendingSocket) {
    return Mono.just(new MyRSocket());
  }
}

class MyRSocket extends AbstractRSocket {
  @Override
  public Flux<Payload> requestStream(Payload payload) {
    return Flux.interval(Duration.ofMillis(200L))
        .map(i -> DefaultPayload.create(payload.getData()));
  }
}

クライアント

クライアントはまず Mono<RSocket> を生成する。これは request-response の場合と変わらない。

final Mono<RSocket> clientMono = RSocketFactory.connect()
    .transport(TcpClientTransport.create("localhost", 7000))
    .start();

取得した RSocketインスタンスrequestStream メソッドでリクエストすると、 Flux が返ってくる。

RSocket rsocket = ...

final Flux<Payload> responseStream = rsocket.requestStream(DefaultPayload.create("hello stream"));