Friday, April 24, 2020

ユーザ、グループと組織の関係に役立つ便利なツール(その2)

前回、ユーザからユーザの所属または所有ロールを取得するための便利なUserBagを紹介しました。今回は、その逆方向、組織の下の全てユーザ、あるロールにアサインした全てのユーザ、またはサイトメンバー全員を取る方法を紹介しようと思います。

実は、Liferayのポータル機能として、ユーザ管理、ロール管理またはサイトメンバー一覧などの画面で、すでに上記機能が実装されていますが、その画面が利用したメソッドは、ポートレットからの呼び出しを前提としたシグネチャーを持っています(すなわち、PortletRequestHttpServletRequestなど)。 Liferayカスタマイズの際、サービスレイヤからそういう機能を実装する場面が多いため、今回の内容は、Liferayの既存機能の中のxxxRequestなどポートレット特化のパラメーターを利用しないこととします(すなわち本来xxxRequestから作成したものをハードコードで作成する)。

UserSearch

LiferayのUsersAdminPortletまたはSiteMembershipsPortletのユーザリスト部分を確認するとユーザ管理のViewUsersManagementToolbarDisplayContext.javaまたはサイトメンバー管理のUsersDisplayContext.java、よくUserSearchというクラスを見ます。さらに、Liferayには、階層構造を用いてデータベースの検索よりサーチエンジンの検索が早いことを利用し、指定条件のユーザを取得できるメソッドが用意されています。

// UsersDisplayContext.java
UserSearch userSearch = new UserSearch(_renderRequest, getPortletURL());
UserSearchTerms searchTerms =
 (UserSearchTerms)userSearch.getSearchTerms();

LinkedHashMap userParams = new LinkedHashMap<>();
userParams.put("inherit", Boolean.TRUE);
userParams.put("usersGroups", Long.valueOf(getGroupId()));

int usersCount = UserLocalServiceUtil.searchCount(
 themeDisplay.getCompanyId(), searchTerms.getKeywords(),
 searchTerms.getStatus(), userParams);

userSearch.setTotal(usersCount);

List users = UserLocalServiceUtil.search(
 themeDisplay.getCompanyId(), searchTerms.getKeywords(),
 searchTerms.getStatus(), userParams, userSearch.getStart(),
 userSearch.getEnd(), userSearch.getOrderByComparator());

userSearch.setResults(users);
そして、UserSearch.javaUserSearchTerms.javaなどのソースを参照し、以下のようなサービスレイヤ向けUserSearchが作成できます。
  • searchTerms.getKeywords()のデフォルト値はnullになる
  • ユーザ全体を取りたい場合、start=0end=総数を指定する
  • userSearch.getOrderByComparator()のデフォルト値はUsersAdminUtil.getUserOrderByComparator("last-name", "asc")になる
Component(
 property = {
  "osgi.command.scope=liferay",
  "osgi.command.function=checkOrganizationUsers",
  "osgi.command.function=checkSiteUsers",
  "osgi.command.function=checkRoleUsers"
 },
 service = SiteUserOrgCmd.class
)
public class SiteUserOrgCmd {

 private static final long companyId = PortalUtil.getDefaultCompanyId();

 public void checkOrganizationUsers(String orgName) throws Exception {

  Organization org = OrganizationLocalServiceUtil.getOrganization(companyId, orgName);

  LinkedHashMap userParams = new LinkedHashMap();
  userParams.put("inherit", Boolean.TRUE);
  userParams.put("usersOrgs", org.getOrganizationId());
  int usersCount = UserLocalServiceUtil.searchCount(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams);
  OrderByComparator obc = UsersAdminUtil.getUserOrderByComparator("last-name", "asc");
  List res = UserLocalServiceUtil.search(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams, 0, usersCount, obc);

  System.out.println("Users of organization " + orgName + ": ");
  for (User u : res) {
   System.out.println("  " + u.getScreenName());
  }

 }

 public void checkSiteUsers(String siteName) throws Exception {

  Group grp = null;
  for (Group g : GroupLocalServiceUtil.getGroups(QueryUtil.ALL_POS, QueryUtil.ALL_POS)) {
   if (g.getName(Locale.getDefault()).equals(siteName)) {
    grp = g;
    break;
   }
  }

  LinkedHashMap userParams = new LinkedHashMap();
  userParams.put("inherit", Boolean.TRUE);
  userParams.put("usersGroups", grp.getGroupId());
  int usersCount = UserLocalServiceUtil.searchCount(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams);
  OrderByComparator obc = UsersAdminUtil.getUserOrderByComparator("last-name", "asc");
  List res = UserLocalServiceUtil.search(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams, 0, usersCount, obc);

  System.out.println("Users of site " + siteName + ": ");
  for (User u : res) {
   System.out.println("  " + u.getScreenName());
  }

 }

 public void checkRoleUsers(String roleName) throws Exception {

  Role role = RoleLocalServiceUtil.getRole(companyId, roleName);

  LinkedHashMap userParams = new LinkedHashMap();
  userParams.put("inherit", Boolean.TRUE);
  userParams.put("usersRoles", role.getRoleId());
  int usersCount = UserLocalServiceUtil.searchCount(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams);
  OrderByComparator obc = UsersAdminUtil.getUserOrderByComparator("last-name", "asc");
  List res = UserLocalServiceUtil.search(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams, 0, usersCount, obc);

  System.out.println("Users of site " + roleName + ": ");
  for (User u : res) {
   System.out.println("  " + u.getScreenName());
  }
 }
}

検証


前回と同じ、検証用エンティティを作成しましょう。

  • ユーザ: ub_user, ub_user2, ub_user3
  • ユーザグループ: ub_grp
  • 組織: ub_org
    • ub_orgの下の階層にob_org1を作成する
  • サイト: ub_site
  • 一般ロール: ub_role

では、上記サービスでUserSearchの効果を検証しましょう。

組織

  •  ub_org <- ub_user2ub_org1 <- ub_user3
g! checkSiteUsers ub_site
Users of site ub_site:
  test
  ub_user
  ub_user3
  ub_user2

  • 前回同様、dxp-14パッチまでのLiferayには、ub_user3を直接にub_orgにアサインしなくで、その下のub_org1にアサインすると、ub_user3ub_siteのサイトメンバーとして認識できなくなります。

ロール

ub_roleを:
  • ub_userにアサインする
  • ub_grpにアサインし、ub_user2ub_grpにアサインする
  • ub_orgにアサインし、ub_user3ub_orgにアサインする
g! checkRoleUsers ub_role
Users of site ub_role:
  ub_user
  ub_user3
  ub_user2

まとめ

今回、Liferayが提供するUserSearchを利用し、サイト、組織またはロールの所属ユーザを全部取得する方法を紹介しました。実は、UserSearchだけではなく、RoleSearchUserGroupSearchGroupSearchなどのsearch系メソッドがLiferay中に定義されています。このようなクラスを同じ手段で改造したら、サービスレイヤであるエンティティーの下のロール、ユーザグループの取得も可能になります。

Tuesday, April 21, 2020

ユーザ、グループと組織の関係に役立つ便利なツール(その1)

Liferayカスタマイズのビジネスロジックに、以下のような処理がよくあると思います。


  • ユーザにアサインしたロールを取得する
  • ユーザの所属組織を取得する
  • ユーザの所属サイトを取得する
  • 組織に所属するユーザを取得する
  • サイトのサイトメンバーを取得する
  • など...


みなさまはこのような処理を行う時、userLocalService.getGroupUsers(グループのユーザを取る)、organizationLocalService.getUserOrganizations(ユーザが所属する組織を取得)などのメソッドをよく利用しますか?実は、これは間違っています。

その原因は、Liferayのサービスから取得できるものは、データベースの中に直接存在するレコードのみです。ただし、組織、サイトは階層構造のため、ユーザがサイトに所属する際、ユーザ-サイトに直接関連するデータベースレコードが存在しないこともできます(例えば、ユーザが所属するユーザグループがサイトメンバとしてサイトにアサインすることができます)。

そのため、データベース上直接関連がないエンティティを取るため、再帰的に階層構造をトラバースしないといけません。そこで、Liferayは階層構造を配慮した便利なツールを提供しています。

今回はユーザからユーザの所属サイト、ユーザグループ、組織またはユーザにアサインしたロールを取るツールを紹介したいと思います。次回は逆方向でサイト、ユーザグループ、組織から所属ユーザを取るツールです。

UserBag

以下のコードでUserBagが作成できます。

UserBag userBag = UserBagFactoryUtil.create(userId);

作成されたUserBagを利用し、以下の処理を行えます。

  • userBag.getRoles()
    • ユーザにアサインしたロールを取得する。
  • userBag.getUserUserGroupIds()
    • ユーザが所属するユーザグループのIDを取得する
  • userBag.getUserOrgs()
    • ユーザが所属する組織を取得する。
  • userBag.getUserGroups()
    • ユーザが所属するサイトを取得する。
  • userBag.getGroups()
    • ユーザが所属するグループを取得する。
※ Liferayには、ユーザグループ、サイトと組織その三つのエンティティに全部groupと関連しています。そのため、getUserGroups()メソッドはユーザグループではなく、ユーザの所属グループをリターンします。
※ 実に、ユーザグループと組織に繋がるgroupは、ユーザグループまたは組織のプロファイルページと見られます。

検証

さて、Liferayを起動してUserBagの効果を検証しましょう。ひとまず、検証用モジュールを作りましょう。

@Component(
 property = {
  "osgi.command.scope=liferay",
  "osgi.command.function=checkUserOrganizations",
  "osgi.command.function=checkUserSites",
  "osgi.command.function=checkUserRoles"
 },
 service = SiteUserOrgCmd.class
)
public class SiteUserOrgCmd {

 private static final long companyId = PortalUtil.getDefaultCompanyId();

 public void checkUserOrganizations(String screenName) throws Exception {

  User user = UserLocalServiceUtil.getUserByScreenName(companyId, screenName);
  UserBag userBag = UserBagFactoryUtil.create(user.getUserId());

  System.out.println("User's organizations in userBag: ");
  for (Organization org : userBag.getUserOrgs()) {
   System.out.println("  " + org.getName());
  }
  System.out.println("User's organizations in service: ");
  for (Organization org : OrganizationLocalServiceUtil.getUserOrganizations(user.getUserId())) {
   System.out.println("  " + org.getName());
  }
 }

 public void checkUserSites(String screenName) throws Exception {

  User user = UserLocalServiceUtil.getUserByScreenName(companyId, screenName);
  UserBag userBag = UserBagFactoryUtil.create(user.getUserId());

  System.out.println("User's sites: ");
  for (Group grp : userBag.getUserGroups()) {
   System.out.println("  " + grp.getName(Locale.getDefault()));
  }

  System.out.println("User's sites in service: ");
  for (Group grp : GroupLocalServiceUtil.getUserGroups(user.getUserId(), true)) {
   System.out.println("  " + grp.getName(Locale.getDefault()));
  }

 }

 public void checkUserRoles(String screenName) throws Exception {

  User user = UserLocalServiceUtil.getUserByScreenName(companyId, screenName);
  UserBag userBag = UserBagFactoryUtil.create(user.getUserId());

  System.out.println("User's roles: ");
  for (Role r : userBag.getRoles()) {
   System.out.println("  " + r.getName());
  }

  System.out.println("User's roles in service: ");
  for (Role r : RoleLocalServiceUtil.getUserRoles(user.getUserId())) {
   System.out.println("  " + r.getName());
  }

 }
}

※ Liferayユーザグループが階層構造になれないため、検証内容から外します。

続いて、検証用エンティティを用意します。
- ユーザ: ub_user
- ユーザグループ: ub_grp
- 組織: ub_org
  - 階層組織: ub_org/ub_org1
- サイト: ub_site
- 一般ロール: ub_role

それては、gogo shellでUserBagの効果を検証しましょう。
※ 検証中、明記以外の場合、「検証の度にユーザ、ロール、サイトと組織の関係を元に戻す」を前提として検証操作をします。

組織

以下の状況を考えてみましょう。
  •  ub_org <- ub_user
    • ub_orgは階層構造中一番トップな組織ため、UserBagOrganizationLocalServiceの戻り値は同じです。
g! checkUserOrganizations ub_user
User's organizations in userBag:
  ub_org
User's organizations in service:
  ub_org

  • ub_org1 <- ub_user
    •   ub_org1ub_orgの一個下の階層の組織のため、ユーザが直接にub_org1のユーザとして追加すると、OrganizationLocalServiceはDBに直接存在するレコードub_org1をリターンします。それに対して、UserBagは階層構造の上の組織ub_orgもリターンします。
g! checkUserOrganizations ub_user
User's organizations in userBag:
  ub_org
  ub_org1
User's organizations in service:
  ub_org1

サイト

以下の状況を考えてみましょう。
  • ub_site <- ub_user
    • ユーザが直接にサイトメンバーとしてサイトへ登録すると、UserBagGroupLocalServiceの出力が同じです。
g! checkUserSites ub_user
User's sites:
  ub_site
User's sites in service:
  ub_site

  • ub_site <- ub_grp, ub_grp <- ub_user
    • ユーザが直接にサイトへ登録ではなく、ユーザグループに通じでサイトメンバーへ登録際、UserBagGroupLocalServiceの出力が同じです。
  • 最初に「あれ?」と思う方がいらっしゃいませんか?
    • 実は、GroupLocalServiceUtil.getUserGroups(long userId, boolean inherit))のパラメーターとしてinherit=trueを指定したら、GroupLocalServiceでもUserBagのような出力ができます。
g! checkUserSites ub_user
User's sites:
  ub_site
User's sites in service:
  ub_site

  • ub_site <- ub_org, ub_org <- ub_user
    • ユーザが直接にサイトへ登録ではなく、ユーザグループに通じでサイトメンバーへ登録際、UserBagGroupLocalServiceの出力が同じです。
    • 理由はユーザグループの場合と同じです。
    • ちなみに、組織サイトも結果に入りました。
g! checkUserSites ub_user
User's sites:
  ub_org
  ub_site
User's sites in service:
  ub_org
  ub_site


  • ub_site <- ub_org, ub_org1 <- ub_user
    • ちょっと変な結果ですが、ub_userを直接に組織ub_orgの下に置かなくて、ub_org下のub_org1にアサインしたとき、ub_userub_siteのサイトメンバーとして認識されないようです。
    • これはLiferayの仕様(liferay-7.1-sp2, dxp-14-7110まで)だそうです(サイトメンバー管理画面にも同じ結果が確認できます)。
g! checkUserSites ub_user
User's sites:
  ub_org1
User's sites in service:
  ub_org1

ロール

以下の状況を考えてみましょう。
  • ub_role <- ub_user
    • ユーザが直接にロールをアサインするとき、UserBagRoleLocalServiceの出力が同じです。
g! checkUserRoles ub_user
User's roles:
  User
  ub_role
User's roles in service:
  User
  ub_role

  • ub_role <- ub_grp, ub_grp <- ub_user
    • ユーザがユーザグループを通じてユub_roleにアサインされるとき、データベースにレコードがいないため、UserBagRoleLocalServiceの出力が違います。
g! checkUserRoles ub_user
User's roles:
  User
  ub_role
User's roles in service:
  User

  • ub_role <- ub_org, ub_org1 <- ub_user
    • ユーザが組織を通じてユub_roleにアサインされるとき、データベースにレコードがいないため、UserBagRoleLocalServiceの出力が違います。
    • ub_org1は階層構造上ub_orgの下の階層のため、ユーザはub_roleを持っています。
g! checkUserRoles ub_user
User's roles:
  User
  ub_role
User's roles in service:
  User

まとめ

今回は、Liferayが提供したUserBagを利用し、ユーザから当該ユーザの所属とロールを取得する方法を紹介しました。UserBagはLiferayの階層構造を配慮して結果をリターンするため、カスタマイズの際役に立つでしょう。また、公式github上のソースコードを参考すると、UserBagをより深く理解できますので(UserBagImpl, UserBagFactoryImpl)、ぜひおすすめします。

ちなみに、UserBagの定義はLiferayのkernelモジュールであるcom.liferay.portal.security.permissionの中にあり、LiferayのpermissionCheckerはよくこのクラスを利用しています。これは本来UserBagを開発する理由でしょう。

Wednesday, April 15, 2020

Alfresco Content Applicationを使ってみよう

こんにちは。てらしたです。

今回はAlfrescoの新しいUIとして開発されているAlfresco Content Applicationをご紹介します。Alfresco Content Applicationは、Alfresco Development Framework(ADF)を使って開発されており、Alfresco上のコンテンツの操作に特化したUIとなっています。GitHubのページに記載されているコンセプトがわかりやすかったので引用しておきます。
The concept of this application is a simple user interface which makes accessing files in the Alfresco Content Services repository easy.
Often Content Management systems provide more capabilities out of the box than most users need; providing too many capabilities to these users prevents them from working efficiently, so they may end up using unsanctioned file management solutions which presents a proliferation of content storage and collaboration solutions as well as compliance issues for organizations.
This application simplifies the complexity of Content Management and provides comprehensive extensibility features for developers, using the Alfresco Application Development Framework, to easily and quickly create custom solutions for specific user cases.
Alfrescoは3.x以降、Alfresco Shareを標準のUIとして新機能の実装や既存機能の改善が行われてきました。その結果、機能が多くなり過ぎて全部乗せのようなUIになってしまっています。ダッシュボード、ブログ、Wiki、カレンダーやワークフローは使わず、コンテンツ管理機能だけ使いたいという場合にはShareではtoo muchなので、Alfresco Content Applicationをカスタマイズして使うというケースが今後増えてくるのではないかと思います。

前置きが長くなりましたが、Alfresco Content Applicationを起動する手順をご紹介します。準備さえできてしまえば本当に簡単に起動できます。
まず準備として、最新のAlfrescoをインストールして起動します。現時点(2020年4月)では201911GAが最新のGA版なので、例えばこちらの記事を参考にしてもらえればと思います。
次に、Node.jsのLTS版(現時点ではv10かv12)をインストールします。以上で準備は完了です。
あとは、GitHubのプロジェクトをcloneして依存モジュールをインストールして起動するだけです。
git clone https://github.com/Alfresco/alfresco-content-app.git
cd alfresco-content-app
npm install
npm start

起動したら自動的にブラウザでログイン画面が開くはずですが、開かない場合は以下のURLを手動で開いてみてください。

http://localhost:4200/

ログインしてサンプルサイトの文書ライブラリを開くと以下のようにフォルダやファイルにアクセスできると思います。

Alfresco Content Applicationの画面

少し使ってみていただければ、コンテンツ操作に特化していてShareに比べてかなりシンプルなUIになっていることがわかるのではないかと思います。
コンテンツ操作に関しては、ファイル/フォルダのアップロードや検索もできますし、新規ボタンから「ライブラリの作成」でサイトを新規に作ってその文書ライブラリにアクセスすることもできます。ただ、サイトメンバーの管理はできないようなので、今のところはそれはShareからやるしかなさそうです。