前: QBPMS のユーザ情報をローカルデータと同期させる(ユーザ情報編)

「ユーザ情報編」では、Questetra BPM Suite の システム設定 API を用いて、ログインに必要な最低限のユーザ情報だけを操作しました。今回は Questetra BPM Suite に登録済みのユーザの所属組織を操作するプログラム例を紹介します。ユーザ情報と同じく「各ユーザがどの組織に所属しているか」を示した TSV ファイルを用意し、それに合わせて Questetra BPM Suite 上の登録情報を同期します。本記事では次のようなフォーマットを前提としています。

questetra@gmail.com00 全社!
questetra+Galapagos@gmail.com20 営業部!
questetra+Oahu@gmail.com20 営業部
questetra+Sumatera@gmail.com10 管理部!
questetra+Canarias@gmail.com10 管理部 30 開発部

最初にユーザの登録メールアドレス、次にユーザが所属する組織をタブ区切りで並べるという形式です。上の例であれば、questetra+Canarias@gmail.com のメールアドレスを持つユーザは「10 管理部」と「30 開発部」の両方に属している、ということになります。また questetra+Galapagos@gmail.com の「20 営業部!」のように組織名の末尾に ! を付けると、そのユーザは組織のリーダであるということを示します。このようなフォーマットの TSV ファイルに Questetra BPM Suite 上のユーザ情報を合わせることが、本記事の目標となります。

ユーザ情報のときと同じく、「TSV ファイルと Questetra BPM Suite 上の登録情報を比較して、異なる部分を変更するクラス」と「API を利用して Questetra BPM Suite 上の所属組織情報を操作するクラス」が必要です。前者を SyncMembershipWithTSV クラス、後者を MembershipDataManager クラスとして実装しましょう。

MembershipDataManager クラス

所属組織情報の操作には Questetra BPM Suite の API ライブラリ com.questetra.bpms.client.swagger から MembershipApi クラス(所属組織情報を操作するクラス)や、QgroupApi クラス(組織情報を操作するクラス)を使用します。また、ユーザ ID を調べるために、「ユーザ情報編」で実装した UserDataManager クラスも使用します。

Basic 認証(コンストラクタ)

import com.questetra.bpms.client.swagger.model.*;
import com.questetra.bpms.client.swagger.ApiClient;
import com.questetra.bpms.client.swagger.ApiException;
import com.questetra.bpms.client.swagger.Configuration;
import com.questetra.bpms.client.swagger.auth.*;
import com.questetra.bpms.client.swagger.api.MembershipApi;
import com.questetra.bpms.client.swagger.api.QgroupApi;

import java.util.HashMap;


/**
 * ユーザの所属組織を列挙・操作する。
 */
class MembershipDataManager {
    private final MembershipApi apiInstance;
    private final HashMap<String, QuserWithPrimaryQgroup> userMap = new HashMap<>();
    private final HashMap<String, Qgroup> groupMap = new HashMap<>();

    public MembershipDataManager() {
        ApiClient defaultClient = Configuration.getDefaultApiClient();
        // URL を設定
        defaultClient.setBasePath("https://example.questetra.net/");

        // Basic 認証
        HttpBasicAuth basic = (HttpBasicAuth) defaultClient.getAuthentication("basic");
        basic.setUsername("{メールアドレス}");
        basic.setPassword("{basic認証用パスワード}");

        this.apiInstance = new MembershipApi();
    }

...

コンストラクタで Basic 認証を行う部分のコードは UserDataManager クラスと変わりありません。今回は所属組織を扱うので、MembershipApi や QgroupApi をインポートしていることに注意してください。

また、次の項で Questetra BPM Suite からユーザオブジェクトや組織オブジェクトを取得するメソッドを実装しますが、そこで使用するメンバ変数 userMap, groupMap を宣言しておきます。これらは一度検索したユーザや組織の情報を記憶しておくための HashMap オブジェクトです。

ユーザ・組織の検索を行うメソッド

     ...

    /**
     * 組織名から組織(Qgroup)を検索する。
     * 指定された名前の組織がQuestetra BPM Suite上に存在しない場合、ApiExceptionをスローする。
     * @param name 組織名。
     * @return Qgroup 検索した組織オブジェクト。
     */
    private Qgroup findGroup(String name) throws ApiException {
        // 既に検索したことのある組織ならばHashMapに追加されている
        if (this.groupMap.containsKey(name)) {
            return this.groupMap.get(name);
        }
        QgroupApi groupApiInstance = new QgroupApi();
        Qgroup result = groupApiInstance.find(null, name).getQgroup();
        // HashMapに追加
        this.groupMap.put(name, result);
        return result;
    }

    /**
     * メールアドレスからユーザ(QuserWithPrimaryQgroup)を検索する。
     * 指定されたメールアドレスを持つユーザがQuestetra BPM Suite上に存在しない場合、
     * ApiExceptionをスローする。
     * @param email メールアドレス。
     * @return QuserWithPrimaryQgroup 検索したユーザオブジェクト。
     */
    private QuserWithPrimaryQgroup findUser(String email) throws ApiException {
        // 既に検索したことのあるユーザならばHashMapに追加されている
        if (this.userMap.containsKey(email)) {
            return this.userMap.get(email);
        }
        UserDataManager userManager = new UserDataManager();
        QuserWithPrimaryQgroup result = userManager.find(null, email).getQuser();
        // HashMapに追加
        this.userMap.put(email, result);
        return result;
    }

    ...

MembershipApi クラスの各種メソッドを使用する際には、ユーザや組織を ID で指定する必要があります。そのため、TSV ファイルに記述されるユーザのメールアドレスからユーザオブジェクトを検索するメソッドと、組織名から組織オブジェクトを検索するメソッドを実装しておきましょう。これらのオブジェクトからインスタンスメソッド getId を呼び出すことで、ユーザ ID・組織 ID を取得することができます。

findUser メソッドでは、「ユーザ情報編」で実装した UserDataManager.find メソッドを使用しています。findUser メソッドは、引数に渡したメールアドレスから、Questetra BPM Suite 上に登録されているユーザオブジェクトを検索して返します。また、一度検索したユーザオブジェクトは、HashMap クラスのメンバ変数 userMap に保存しておきましょう。HashMap を用いることで、2 回目以降の検索時はこちらからユーザオブジェクトを取り出すことが出来ます。

findGroup メソッドでは、API ライブラリからインポートしておいた QgroupApi クラスを使用しています。QgroupApi クラスは組織情報の操作を行うクラスで、QgroupApi.find メソッドは組織オブジェクトの検索を行うメソッドです。引数は組織 ID id, 組織名 name で、指定するのは片方だけで構いません。今回は組織 ID がわからないので、引数 id には null を渡しましょう。また組織オブジェクトもユーザオブジェクトと同じく、HashMap に保存しておきましょう。

ユーザの所属組織を列挙するメソッド

    ...

    /**
     * ユーザの所属組織を列挙する。
     * @param email ユーザのメールアドレス。
     * @return MembershipList 所属組織のリスト。
     */
    public MembershipList findMemberships(String email) throws ApiException {
        // 指定したメールアドレスのユーザIDを検索
        Long userId = this.findUser(email).getId();
        MembershipList result = this.apiInstance.listByQuser(userId);
        return result;
    }

    ...

ユーザを指定すると、そのユーザが所属している組織一覧を返すメソッドです。TSV ファイルと Questetra BPM Suite 上の登録情報で所属組織を比較するために実装します。

MembershipApi クラスには、ユーザ ID から所属組織一覧を検索する listByQuser メソッドが実装されています。MembershipApi.listByQuser メソッドの引数はユーザ ID id です。先ほどの findUser メソッドを用いてユーザオブジェクトを検索すれば、ユーザのメールアドレスからユーザ ID を調べることができます。

ユーザの所属組織を追加するメソッド

    ...

    /**
     * ユーザに所属組織情報を追加する。
     * @param email ユーザのメールアドレス。
     * @param group 組織名。
     * @param isLeader リーダならtrue, スタッフならfalse。
     * @return MembershipWrapper
     */
    public MembershipWrapper add(String email, String group,
                                 boolean isLeader) throws ApiException {
        // 指定したメールアドレスのユーザIDを検索
        Long userId = this.findUser(email).getId();
        // 指定した組織名の組織IDを検索
        Long groupId = this.findGroup(group).getId();

        MembershipWrapper result;

        if (isLeader) {
            result = this.apiInstance.add(userId, groupId, "_leader");
            System.out.printf("Add: %s / %s, %s\n", email, group, "leader");
        } else {
            result = this.apiInstance.add(userId, groupId, null);
            System.out.printf("Add: %s / %s, %s\n", email, group, "staff");
        }

        return result;
    }

    ...

ユーザに所属組織を追加するメソッドです。引数にユーザのメールアドレス email と組織名 group、リーダか否かを表す isLeader を渡すと、当該のユーザの所属に組織が追加されます。引数 isLeader は boolean で、true ならユーザがその組織のリーダであることを示しています。

所属組織の追加には MembershipApi クラスの add メソッドを利用しています。引数はユーザ ID quserId, 組織 ID qgroupId, 組織での立場 role の 3 つです。また、ユーザ ID と組織 ID を調べるために、先ほど実装した findUser, findGroup メソッドを用いています。

ユーザの所属組織を削除するメソッド

    ...

    /**
     * ユーザの所属組織情報を削除する。
     * @param email ユーザのメールアドレス。
     * @param group 組織名。
     */
    public void delete(String email, String group) throws ApiException {
        // 指定したメールアドレスのユーザIDを検索
        Long userId = this.findUser(email).getId();
        // 指定した組織名の組織IDを検索
        Long groupId = this.findGroup(group).getId();

        this.apiInstance.delete(userId, groupId);
        System.out.printf("Deleted: %s / %s\n", email, group);
    }

    ...

ユーザの所属組織を削除するメソッドです。引数にユーザのメールアドレス email と組織名 group を渡すと、当該のユーザの所属からその組織が削除されます。

所属組織の削除には MembershipApi クラスの delete メソッドを利用しています。引数はユーザ ID quserId, 組織 ID qgroupId の 2 つです。add メソッドと同じく、ユーザ ID と組織 ID は findUser, findGroup メソッドを用いて調べています。

ユーザの所属組織情報を更新するメソッド

    ...

    /**
     * ユーザの所属組織情報を更新する。
     * @param email ユーザのメールアドレス。
     * @param group 組織名。
     * @param isLeader リーダならtrue, スタッフならfalse。
     * @return MembershipWrapper
     */
    public MembershipWrapper update(String email, String group,
                                    boolean isLeader) throws ApiException {
        // 指定したメールアドレスのユーザIDを検索
        Long userId = this.findUser(email).getId();
        // 指定した組織名の組織IDを検索
        Long groupId = this.findGroup(group).getId();

        MembershipWrapper result;

        // リーダに変更
        if (isLeader) {
            result = this.apiInstance.update(userId, groupId, "_leader");
            System.out.printf("Changed the role: %s / %s, → %s\n",
                              email, group, "leader");
        }
        // スタッフに変更
        else {
            result = this.apiInstance.update(userId, groupId, null);
            System.out.printf("Changed the role: %s / %s, → %s\n",
                              email, group, "staff");
        }

        return result;
    }

}

ユーザの所属組織における立場を更新するメソッドです。引数にユーザのメールアドレス email と、組織名 group、リーダか否かを表す isLeader を渡すと、当該のユーザの所属組織における立場が更新されます。isLeader は add メソッドの引数と同じく、true ならユーザがその組織のリーダになることを示しています。

所属組織情報の更新には MembershipApi クラスの update メソッドを利用しています。引数はユーザ ID quserId, 組織 ID qgroupId, 組織での立場 role の 3 つです。使い方は add メソッドとほとんど同じです。MembershipApi.update メソッドを用いて、例えば『今までは所属組織において「スタッフ」だったユーザを「リーダ」に変更する』といった操作ができます。

SyncMembershipWithTSV クラス

TSV ファイルと Questetra BPM Suite 上の登録情報を比較して、異なる部分を揃える処理を行います。TSV ファイルと Questetra BPM Suite からそれぞれ所属組織情報を読み込み、登録情報を変更すべき部分を決定し、Web API を通じて変更処理をすることで、ローカルデータと Questetra BPM Suite 間での所属組織情報の同期を実現します。

main メソッド

import com.questetra.bpms.client.swagger.ApiException;
import com.questetra.bpms.client.swagger.model.Membership;

import java.io.IOException;
import java.util.*;

import static java.util.stream.Collectors.toSet;

class SyncMembershipWithTSV {
    public static void main(String[] args) throws IOException, ApiException {
        // TSVデータを読み込む
        List<String[]> data = TSVReader.readLines({TSV ファイルのパス});
        // 1行分ずつ処理する
        for (String[] user : data) {
            sync(user);
        }
    }
    ...

基本の動作は「TSV ファイルを読み込み→ 1 行(1 ユーザ)ずつ同期処理」になります。TSV ファイルの読み込み方法は「ユーザ情報編」と同様に、TSVReader クラスを用いて行います。読み込まれたデータは [メールアドレス, 組織0, 組織1, …] という String[] の List になっています。1 ユーザ分の同期処理は sync メソッドにまとめられています。

sync (String[] user) メソッド

このメソッドでは、1 ユーザ分の所属組織情報の同期処理を行います。引数 user は TSVReader.readLines() で読み込んだ TSV ファイルの 1 行分のデータです。

Questetra BPM Suite 上での各ユーザの所属組織を調べる

    ...

    /**
     * ユーザ1人の所属組織情報を同期する
     * @param user TSVから読み込んだ1行分のデータ
     * @throws ApiException
     */
    private static void sync(String[] user) throws ApiException {
        MembershipDataManager manager = new MembershipDataManager();

        // Questetra BPM Suite上で現在ユーザが所属している組織を調べる
        Set<Membership> currentMembershipSet = new HashSet<>(
                            manager.findMemberships(user[0]).getMemberships());
        // 「リーダとして所属している組織」のSetを作成
        Set<Membership> currentLeaderSet =
                            currentMembershipSet.stream()
                                      .filter(m -> Objects.equals(
                                              m.getRole(),Membership.RoleEnum.LEADER))
                                      .collect(toSet());

        Set<String> currentGroupSet = currentMembershipSet.stream()
                                                      .map(Membership::getQgroupName)
                                                      .collect(toSet());
        Set<String> currentLeaderGroupSet = currentLeaderSet.stream()
                                                      .map(Membership::getQgroupName)
                                                      .collect(toSet());

    ...

まず、ユーザが Questetra BPM Suite 上ではどの組織に所属しているか、MembershipDataManager.findMemberships メソッドを用いて調べます。ユーザが現在 Questetra BPM Suite 上で所属している組織の情報は、Membership オブジェクトの集合で得られます。後で Set<String> 型同士での比較を行って変更する箇所を決定するので、Membership オブジェクトから組織名を取得し Set<String> 型の currentGroupSet を作成します。

また、組織での立場に変更がある場合を調べるために、Questetra BPM Suite 上でリーダとして所属している組織の集合も用意します。取得した Membership オブジェクト集合に Stream API を用いて「リーダとして所属している組織のみを抽出する」フィルタをかけ、残った Membership オブジェクトから組織名を取得することで、Set<String> 型の currentLeaderGroupSet を作成します。

TSV ファイルでの各ユーザの所属組織を調べる

    ...

        // TSVファイルでユーザが所属している組織を調べる
        Set<String> nextMembershipSet = new HashSet<>(Arrays.asList(user)
                                                            .subList(1, user.length));
        // 「リーダとして所属している組織」のSetを作成
        Set<String> nextLeaderGroupSet =
                nextMembershipSet.stream()
                                 .filter(m -> m.endsWith("!"))
                                 .map(m -> m.substring(0, m.length()-1))
                                 .collect(toSet());
        Set<String> nextGroupSet = nextMembershipSet.stream()
                                    .map(m -> {
                                        if (m.endsWith("!"))
                                            return m.substring(0, m.length()-1);
                                        else return m;}).collect(toSet());

        sync(user[0], currentGroupSet, nextGroupSet,
                      currentLeaderGroupSet, nextLeaderGroupSet);
    }

    ...

同じように、TSV ファイルに記述された所属組織情報もまとめます。今回、TSV ファイルで組織名の末尾に ! が付与されている場合は、ユーザはその組織のリーダであるとみなすことにしています。

Questetra BPM Suite 上に存在しないユーザのメールアドレスや組織名を TSV ファイル内に記述した場合、findUser メソッドや findGroup メソッドから ApiException 例外がスローされます。

最後に作成した組織集合を sync (String email, Set<String> currentGroupSet, …) メソッドに渡して、Web API 経由で登録情報を操作します。

sync (String email, Set<String> currentGroupSet, …) メソッド

このメソッドでは、所属組織情報を変更すべき箇所を特定し、実際に Web API を通じて Questetra BPM Suite 上に登録された情報を変更します。

「所属を追加・削除する組織」を探す

    ...

    /**
     * Questetra BPM Suite上とTSV上の所属組織情報を比較して、Web API経由で登録情報を操作する。
     * @param email ユーザのメールアドレス
     * @param currentGroupSet Questetra BPM Suite上で現在所属している組織集合
     * @param nextGroupSet TSVで示された所属組織集合
     * @param currentLeaderGroupSet Questetra BPM Suite上で
     *                              現在リーダとして所属している組織集合
     * @param nextLeaderGroupSet TSVで示された、リーダとして所属する組織集合
     * @throws ApiException
     */
    private static void sync(String email,
                             Set<String> currentGroupSet,
                             Set<String> nextGroupSet,
                             Set<String> currentLeaderGroupSet,
                             Set<String> nextLeaderGroupSet) throws ApiException {
        MembershipDataManager manager = new MembershipDataManager();

        // 「TSVにしかない組織名(→追加)」,
        // 「Questetra BPM Suite上にしかない組織名(→削除)」のSetを作成

        // 削除する組織名の集合
        Set<String> willDelete = subtract(currentGroupSet, nextGroupSet);
        // 追加する組織名の集合
        Set<String> willAdd = subtract(nextGroupSet, currentGroupSet);

        for (String group : willDelete) { // 所属から削除
            manager.delete(email, group);
        }

        for (String group : willAdd) { // 所属に追加
            if (nextLeaderGroupSet.contains(group))
                 manager.add(email, group, true);
            else manager.add(email, group, false);
        }
  
    ...

「ユーザ情報編」におけるユーザ情報の差分の調べ方と同じように、今回も集合演算を用いて所属組織の差分を調べます。まずは所属組織が変わっているかどうかを調べ、ユーザの所属に追加する組織と、所属から削除する組織を決定します。

今回は 2 つの集合を引数に渡すとそれらの差集合を返す subtract メソッドを実装して使っています。Questetra BPM Suite 上で現在所属している組織のうち、TSV ファイルの内容と重複していないものの集合は Set<String> 型の willDelete に格納して削除対象とします。同様に TSV ファイルにしか含まれず、所属組織に追加しなければならない組織の集合 willAdd も作成します。

そして willDelete, willAdd に格納されている各組織について、MembershipDataManager クラスの delete, add メソッドを用いて Questetra BPM Suite 上の情報を変更します。これで、所属する組織を Questetra BPM Suite と TSV ファイルで揃えることができます。

「立場が変わった組織」を探す

    ...

        // Questetra BPM Suite上ではリーダだがTSV上ではそうではない所属組織を求め、
        // 引き続き所属する組織のみを残す
        // 「リーダだったのが、リーダでなくなる」=「リーダからスタッフに変わる」
        //                                  or「組織のメンバから外れる(willDelete)」
        Set<String> wasLeader = subtract(currentLeaderGroupSet, nextLeaderGroupSet);
        Set<String> toStaff = subtract(wasLeader, willDelete);

        // TSV上ではリーダだがQuestetra BPM Suite上ではそうではない所属組織を求め、
        // 引き続き所属する組織のみを残す
        // 「新しくリーダになる」=「スタッフからリーダに変わる」
        //                      or「新しく組織にリーダとして追加される(willAdd)」
        Set<String> willLeader = subtract(nextLeaderGroupSet, currentLeaderGroupSet);
        Set<String> toLeader = subtract(willLeader, willAdd);

        for (String group : toStaff) { // スタッフに変更
            manager.update(email, group, false);
        }

        for (String group : toLeader) { // リーダに変更
            manager.update(email, group, true);
        }
    }

    ...

次に、「同じ組織に引き続き所属するが、立場が変わった」というケースがないか調べます。同期処理の前後で所属関係が変化する組織は、前項で willDelete, willAdd として求められています。この集合に含まれておらず、かつユーザの立場が変わっている組織がないかを調べます。

こちらも Questetra BPM Suite 上の情報 currentLeaderGroupSet と TSV ファイル上の情報 nextLeaderGroupSet で差分を取ることで、立場の情報に何らかの変更があった組織を見つけることができます。しかし、例えば「リーダを務める組織集合から組織名がなくなった」とき、原因は「リーダからスタッフに立場が変わった」と「その組織に所属しなくなった」の 2 通りが考えられます。この場合、「所属しなくなる組織の集合」である willDelete と差を取ることで、前者のケースのみに絞り込むことができます。これはスタッフ→リーダへ変更する組織集合を求める際にも同じように考えられます。

スタッフに変更する組織集合 toStaff, リーダに変更する組織集合 toLeader が求められたら、MembershipDataManager クラスの update メソッドを用いて Questetra BPM Suite 上の登録情報を変更します。これで所属組織内での立場も同期することができます。

subtract メソッド

    /**
     * 2つの集合の差(set1-set2)を返す。
     * @param set1 引かれる集合
     * @param set2 引く集合
     * @param <T>
     * @return 差集合
     */
    private static <T> Set<T> subtract(Set<T> set1, Set<T> set2) {
        Set<T> subtraction = new HashSet<>(set1);
        subtraction.removeAll(set2);
        return subtraction;
    }

集合演算に用いているメソッドです。渡された 2 つの集合の差集合を返します。


「ユーザ情報編」と合わせて、これで基本的な Questetra BPM Suite のユーザ情報を外部ファイルと同期することができるようになりました。ユーザ詳細ページでいうと、下のスクリーンショットで囲んだ部分に当たります。

Web API を利用してのユーザ情報操作についての記事は今回で終わりになります。Questetra BPM Suite には今回紹介したもの以外にも様々な API エンドポイントが提供されています。com.questetra.bpms.client.swagger のドキュメントは以下のリンクで公開されていますので、ぜひ参考にされてください。


付録: 使用コード全文

SyncMembershipWithTSV.java

import com.questetra.bpms.client.swagger.ApiException;
import com.questetra.bpms.client.swagger.model.Membership;

import java.io.IOException;
import java.util.*;

import static java.util.stream.Collectors.toSet;

class SyncMembershipWithTSV {
    public static void main(String[] args) throws IOException, ApiException {
        // TSVデータを読み込む
        List<String[]> data = TSVReader.readLines("membership_test2.tsv");
        // 1行分ずつ処理する
        for (String[] user : data) {
            sync(user);
        }
    }


    /**
     * ユーザ1人の所属組織情報を同期する
     * @param user TSVから読み込んだ1行分のデータ
     * @throws ApiException
     */
    private static void sync(String[] user) throws ApiException {
        MembershipDataManager manager = new MembershipDataManager();

        // Questetra BPM Suite上で現在ユーザが所属している組織を調べる
        Set<Membership> currentMembershipSet = new HashSet<>(
                            manager.findMemberships(user[0]).getMemberships());
        // 「リーダとして所属している組織」のSetを作成
        Set<Membership> currentLeaderSet =
                            currentMembershipSet.stream()
                                      .filter(m -> Objects.equals(
                                              m.getRole(),Membership.RoleEnum.LEADER))
                                      .collect(toSet());

        Set<String> currentGroupSet = currentMembershipSet.stream()
                                                      .map(Membership::getQgroupName)
                                                      .collect(toSet());
        Set<String> currentLeaderGroupSet = currentLeaderSet.stream()
                                                      .map(Membership::getQgroupName)
                                                      .collect(toSet());

        // TSVファイルでユーザが所属している組織を調べる
        Set<String> nextMembershipSet = new HashSet<>(Arrays.asList(user)
                                                            .subList(1, user.length));
        // 「リーダとして所属している組織」のSetを作成
        Set<String> nextLeaderGroupSet =
                nextMembershipSet.stream()
                                 .filter(m -> m.endsWith("!"))
                                 .map(m -> m.substring(0, m.length()-1))
                                 .collect(toSet());
        Set<String> nextGroupSet = nextMembershipSet.stream()
                                    .map(m -> {
                                        if (m.endsWith("!"))
                                            return m.substring(0, m.length()-1);
                                        else return m;}).collect(toSet());

        sync(user[0], currentGroupSet, nextGroupSet,
                      currentLeaderGroupSet, nextLeaderGroupSet);
    }

    /**
     * Questetra BPM Suite上とTSV上の所属組織情報を比較して、Web API経由で登録情報を操作する。
     * @param email ユーザのメールアドレス
     * @param currentGroupSet Questetra BPM Suite上で現在所属している組織集合
     * @param nextGroupSet TSVで示された所属組織集合
     * @param currentLeaderGroupSet Questetra BPM Suite上で
     *                              現在リーダとして所属している組織集合
     * @param nextLeaderGroupSet TSVで示された、リーダとして所属する組織集合
     * @throws ApiException
     */
    private static void sync(String email,
                             Set<String> currentGroupSet,
                             Set<String> nextGroupSet,
                             Set<String> currentLeaderGroupSet,
                             Set<String> nextLeaderGroupSet) throws ApiException {
        MembershipDataManager manager = new MembershipDataManager();

        // 「TSVにしかない組織名(→追加)」,
        // 「Questetra BPM Suite上にしかない組織名(→削除)」のSetを作成

        // 削除する組織名の集合
        Set<String> willDelete = subtract(currentGroupSet, nextGroupSet);
        // 追加する組織名の集合
        Set<String> willAdd = subtract(nextGroupSet, currentGroupSet);

        for (String group : willDelete) { // 所属から削除
            manager.delete(email, group);
        }

        for (String group : willAdd) { // 所属に追加
            if (nextLeaderGroupSet.contains(group))
                 manager.add(email, group, true);
            else manager.add(email, group, false);
        }


        // Questetra BPM Suite上ではリーダだがTSV上ではそうではない所属組織を求め、
        // 引き続き所属する組織のみを残す
        // 「リーダだったのが、リーダでなくなる」=「リーダからスタッフに変わる」
        //                                  or「組織のメンバから外れる(willDelete)」
        Set<String> wasLeader = subtract(currentLeaderGroupSet, nextLeaderGroupSet);
        Set<String> toStaff = subtract(wasLeader, willDelete);

        // TSV上ではリーダだがQuestetra BPM Suite上ではそうではない所属組織を求め、
        // 引き続き所属する組織のみを残す
        // 「新しくリーダになる」=「スタッフからリーダに変わる」
        //                      or「新しく組織にリーダとして追加される(willAdd)」
        Set<String> willLeader = subtract(nextLeaderGroupSet, currentLeaderGroupSet);
        Set<String> toLeader = subtract(willLeader, willAdd);

        for (String group : toStaff) { // スタッフに変更
            manager.update(email, group, false);
        }

        for (String group : toLeader) { // リーダに変更
            manager.update(email, group, true);
        }
    }


    /**
     * 2つの集合の差(set1-set2)を返す。
     * @param set1 引かれる集合
     * @param set2 引く集合
     * @param <T>
     * @return 差集合
     */
    private static <T> Set<T> subtract(Set<T> set1, Set<T> set2) {
        Set<T> subtraction = new HashSet<>(set1);
        subtraction.removeAll(set2);
        return subtraction;
    }
}

MembershipDataManager.java

import com.questetra.bpms.client.swagger.api.QgroupApi;
import com.questetra.bpms.client.swagger.model.*;
import com.questetra.bpms.client.swagger.ApiClient;
import com.questetra.bpms.client.swagger.ApiException;
import com.questetra.bpms.client.swagger.Configuration;
import com.questetra.bpms.client.swagger.auth.*;
import com.questetra.bpms.client.swagger.api.MembershipApi;

import java.util.HashMap;


/**
 * ユーザの所属組織を列挙・操作する。
 */
class MembershipDataManager {
    private final MembershipApi apiInstance;
    private final HashMap<String, QuserWithPrimaryQgroup> userMap = new HashMap<>();
    private final HashMap<String, Qgroup> groupMap = new HashMap<>();

    public MembershipDataManager() {
        ApiClient defaultClient = Configuration.getDefaultApiClient();
        // URL を設定
        defaultClient.setBasePath("https://example.questetra.net/");

        // Basic 認証
        HttpBasicAuth basic = (HttpBasicAuth)defaultClient
                                                .getAuthentication("basic");
        basic.setUsername({メールアドレス});
        basic.setPassword({basic認証パスワード});

        this.apiInstance = new MembershipApi();
    }

    /**
     * ユーザの所属組織を列挙する。
     * @param email ユーザのメールアドレス。
     * @return MembershipList 所属組織のリスト。
     */
    public MembershipList findMemberships(String email) throws ApiException {
        // 指定したメールアドレスのユーザIDを検索
        Long userId = this.findUser(email).getId();
        MembershipList result = this.apiInstance.listByQuser(userId);
        return result;
    }

    /**
     * 組織名から組織(Qgroup)を検索する。
     * 指定された名前の組織がQuestetra BPM Suite上に存在しない場合、ApiExceptionをスローする。
     * @param name 組織名。
     * @return Qgroup 検索した組織オブジェクト。
     */
    private Qgroup findGroup(String name) throws ApiException {
        // 既に検索したことのある組織ならばHashMapに追加されている
        if (this.groupMap.containsKey(name)) {
            return this.groupMap.get(name);
        }
        QgroupApi groupApiInstance = new QgroupApi();
        Qgroup result = groupApiInstance.find(null, name).getQgroup();
        // HashMapに追加
        this.groupMap.put(name, result);
        return result;
    }

    /**
     * メールアドレスからユーザ(QuserWithPrimaryQgroup)を検索する。
     * 指定されたメールアドレスを持つユーザがQuestetra BPM Suite上に存在しない場合、
     * ApiExceptionをスローする。
     * @param email メールアドレス。
     * @return QuserWithPrimaryQgroup 検索したユーザオブジェクト。
     */
    private QuserWithPrimaryQgroup findUser(String email) throws ApiException {
        // 既に検索したことのあるユーザならばHashMapに追加されている
        if (this.userMap.containsKey(email)) {
            return this.userMap.get(email);
        }
        UserDataManager userManager = new UserDataManager();
        QuserWithPrimaryQgroup result = userManager.find(null, email).getQuser();
        // HashMapに追加
        this.userMap.put(email, result);
        return result;
    }

    /**
     * ユーザに所属組織情報を追加する。
     * @param email ユーザのメールアドレス。
     * @param group 組織名。
     * @param isLeader リーダならtrue, スタッフならfalse。
     * @return MembershipWrapper
     */
    public MembershipWrapper add(String email, String group,
                                 boolean isLeader) throws ApiException {
        // 指定したメールアドレスのユーザIDを検索
        Long userId = this.findUser(email).getId();
        // 指定した組織名の組織IDを検索
        Long groupId = this.findGroup(group).getId();

        MembershipWrapper result;

        if (isLeader) {
            result = this.apiInstance.add(userId, groupId, "_leader");
            System.out.printf("Add: %s / %s, %s\n", email, group, "leader");
        } else {
            result = this.apiInstance.add(userId, groupId, null);
            System.out.printf("Add: %s / %s, %s\n", email, group, "staff");
        }

        return result;
    }

    /**
     * ユーザの所属組織情報を削除する。
     * @param email ユーザのメールアドレス。
     * @param group 組織名。
     */
    public void delete(String email, String group) throws ApiException {
        // 指定したメールアドレスのユーザIDを検索
        Long userId = this.findUser(email).getId();
        // 指定した組織名の組織IDを検索
        Long groupId = this.findGroup(group).getId();

        this.apiInstance.delete(userId, groupId);
        System.out.printf("Deleted: %s / %s\n", email, group);
    }

    /**
     * ユーザの所属組織情報を更新する。
     * @param email ユーザのメールアドレス。
     * @param group 組織名。
     * @param isLeader リーダならtrue, スタッフならfalse。
     * @return MembershipWrapper
     */
    public MembershipWrapper update(String email, String group,
                                    boolean isLeader) throws ApiException {
        // 指定したメールアドレスのユーザIDを検索
        Long userId = this.findUser(email).getId();
        // 指定した組織名の組織IDを検索
        Long groupId = this.findGroup(group).getId();

        MembershipWrapper result;

        // リーダに変更
        if (isLeader) {
            result = this.apiInstance.update(userId, groupId, "_leader");
            System.out.printf("Changed the role: %s / %s, → %s\n",
                              email, group, "leader");
        }
        // スタッフに変更
        else {
            result = this.apiInstance.update(userId, groupId, null);
            System.out.printf("Changed the role: %s / %s, → %s\n",
                              email, group, "staff");
        }

        return result;
    }

}