社内勉強会でLombokについて発表してきました。
www.slideshare.net
Lombok便利ですよね!もぅLombok無しには戻れません。
Redmine 3.4.0、3.4.1 がリリースされました。(3.4.0が7月2日、バグフィックスで3.4.1が7月9日)
View Customizeだけは動作確認済みですが、他のプラグインも後ほど確認します。
いつもどおりVagrantのboxもあげておきました。
カスタムフィールドの値に応じて行を装飾する方法について問い合わせをいただいたので、「カスタムフィールド1の値が'A'の場合には、行のフォントを太字に変える」をView customizeで行ってみます。
チケットの優先度など、デフォルトで存在するフィールドについては、もとからclassが振られているのでCSSで装飾しやすいですが、カスタムフィールドだとclassが振られていないので、CSSだけではできません。 そのためJavaScriptにて値を判定して、スタイルを設定することによって対応します。
チケット一覧を対象にします。
/issues$
Type:JavaScript
として下記を設定します。cf_1
の部分は、該当のカスタムフィールドに応じて変更してください。
$(function() { $('table.issues td.cf_1') .filter(function() { return $(this).text() == 'A'; }) .parent() .css('color', 'red'); });
一覧に該当のカスタムフィールドが表示されていないと判定ができないので、ご注意ください。
複数の属性を指定するような場合だと、JavaScriptだと面倒かと思います。そういった場合には、JavaScript側ではclassを追加するだけで、そのclassに応じたCSSを別途定義しておくような形がよいかと思います。
追記@2017-07-02
下記Issueで既に対応していただいています。次のバージョン(3.4.5)に入る予定です。
MyBatisのSQLへのパラメータ埋め込みで、size
という名前を使うと、JavaTypeがintとなってしまうようです。
現象を確認したのは、mybatis-spring-boot-starter
の1.3.0(MyBatis自体のバージョンは3.4.4)です。
下記のようなMapperを作ります。size
という名前で、long型の引数をマッピングします。
package com.example.demo; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; @Repository @Mapper public interface TestRepository { @Insert("INSERT INTO test(number, size) VALUES(#{number}, #{size})") public void insert(@Param("number") long number, @Param("size") long size); }
上記のメソッドを呼び出すと、下記のようなエラーが発生します。
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='size', mode=IN, javaType=int, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}. Cause: org.apache.ibatis.type.TypeException: Error setting non null for parameter #2 with JdbcType null . Try setting a different JdbcType for this parameter or a different configuration property. Cause: java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.Integer
なぜかsize
がintとしてマッピングされようとしてClassCastExceptionが発生しています。LongTypeHandler
が使われるべきなのに、IntTypeHandler
が使用されてしまっているようです。
試しに下記のように別の名前に変えるとうまく動作します。
@Insert("INSERT INTO test(number, size) VALUES(#{number}, #{size2})") public void insert2(@Param("number") long number, @Param("size2") long size);
size
という名前は、何か特別なものとして使われていて、それと競合することによっておきてしまっているのかもしれません。
Redmine 3.3時点では、ニュースの参照に関する権限が無く、一部のユーザにニュースを見せないということが出来ません。
@onozaty Feature #7068 が採用されればRedmine 3.4.0でロールごとにニュースの参照が制限できるようになります。 https://t.co/fXkrc4UB5E
— MAEDA, Go (@g_maeda) 2017年5月11日
それをつぶやいたところ、前田さんにRedmine本家のFeatureにあがっていて、3.4.0で入るかもといった情報を頂いたので、先行してパッチを当てて試してみました。
現時点のパッチは下記になっています。
軽微な変更だったのと、最新じゃないバージョンに当てたかったので、手作業で差分当てました。下記の3ファイル(合計3行)さえ直せば、すぐに試せます。(日本語の表示確認してなくて良ければ、ja.yml
もいらないです)
diff --git a/config/locales/en.yml b/config/locales/en.yml index a8eca2dfe..f2a8cbc0b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -497,6 +497,7 @@ en: permission_view_time_entries: View spent time permission_edit_time_entries: Edit time logs permission_edit_own_time_entries: Edit own time logs + permission_view_news: View news permission_manage_news: Manage news permission_comment_news: Comment news permission_view_documents: View documents
diff --git a/config/locales/ja.yml b/config/locales/ja.yml index a018e7240..8d1ae6ce1 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -427,6 +427,7 @@ ja: permission_edit_time_entries: 作業時間の編集 permission_edit_own_time_entries: 自身が記入した作業時間の編集 permission_manage_project_activities: 作業分類 (時間管理) の管理 + permission_view_news: ニュースの閲覧 permission_manage_news: ニュースの管理 permission_comment_news: ニュースへのコメント permission_view_documents: 文書の閲覧
diff --git a/lib/redmine.rb b/lib/redmine.rb index e3ff60fd1..204429cc6 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -131,7 +131,7 @@ Redmine::AccessControl.map do |map| end map.project_module :news do |map| - map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true + map.permission :view_news, {:news => [:index, :show]}, :read => true map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy], :attachments => :upload}, :require => :member map.permission :comment_news, {:comments => :create} end
ロールの編集画面に「ニュースの閲覧」という権限が追加されて、制御可能になりました。実際のニュースの表示もばっちりです。
こないだSTOMP over WebSocketを試してみましたが、今度はSTOMPを使わずに、TextWebSocketHandler
を使ったWebSocketを試してみます。題材も同じくチャットです。
ルームの情報をどのように渡そうか悩みました。コードを書き始めるまでは、WebSocketで接続する先にヘッダとかで渡せばいいかなと思っていましたが、調べてみたら渡せなかったので、やもえずURLのクエリパラメータとして渡すようにしました。
テキストでのやり取りを行うので、TextWebSocketHandler
を利用します。TextWebSocketHandler
の各メソッドをoverrideして必要な処理を実装するだけです。
ルームの情報は、URLのクエリとしてクライアントから送っているので、接続が確立したタイミング(afterConnectionEstablished
)にて、ルーム毎にWebSocketSession
を保持するようにします。
メッセージを受け取ったら、自分のルームと同じWebSocketSession
に対して、メッセージを送るだけです。
package com.example; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; @Component public class ChatHandler extends TextWebSocketHandler { private ConcurrentHashMap<String, Set<WebSocketSession>> roomSessionPool = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String roomName = session.getUri().getQuery(); roomSessionPool.compute(roomName, (key, sessions) -> { if (sessions == null) { sessions = new CopyOnWriteArraySet<>(); } sessions.add(session); return sessions; }); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String roomName = session.getUri().getQuery(); for (WebSocketSession roomSession : roomSessionPool.get(roomName)) { roomSession.sendMessage(message); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String roomName = session.getUri().getQuery(); roomSessionPool.compute(roomName, (key, sessions) -> { sessions.remove(session); if (sessions.isEmpty()) { // 1件もない場合はMapからクリア sessions = null; } return sessions; }); } }
WebSocketの設定として、URLとHandlerを紐付けます。
package com.example; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import lombok.AllArgsConstructor; @Configuration @EnableWebSocket @AllArgsConstructor public class WebSocketConfig implements WebSocketConfigurer { private final ChatHandler chatHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatHandler, "/endpoint"); } }
クライアント側では、下記のようなコードになりました。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>チャット</title> <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap.min.css" /> <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css" /> </head> <body> <div class="container"> <h2>チャット</h2> <div class="form-horizontal"> <div class="form-group"> <label for="roomName" class="col-sm-2 control-label">ルーム</label> <div class="col-sm-2"> <input id="roomName" type="text" class="form-control" value="example" /> </div> <div class="col-sm-3"> <button id="connectButton" type="button" class="btn btn-default">接続</button> <button id="disconnectButton" class="btn btn-default">切断</button> </div> </div> <div class="form-group"> <label for="message" class="col-sm-2 control-label">メッセージ</label> <div class="col-sm-4"> <input id="message" type="text" class="form-control" /> </div> <div class="col-sm-2"> <button id="sendButton" type="button" class="btn btn-default">送信</button> </div> </div> <div class="row"> <div class="col-sm-4 col-sm-offset-2"> <ul id="messageList" class="list-unstyled"> </ul> </div> </div> </div> </div> <script src="/webjars/jquery/1.12.4/jquery.min.js"></script> <script src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script> $(function() { var endpoint = 'ws://' + location.host + '/endpoint'; var webSocket = null; $('#connectButton').click(function() { $("#messageList").empty(); webSocket = new WebSocket(endpoint + '?' + encodeURIComponent($('#roomName').val())); webSocket.onopen = function() { $('#roomName').prop('disabled', true); $('#connectButton').prop('disabled', true); $('#disconnectButton').prop('disabled', false); }; webSocket.onclose = function() { }; webSocket.onmessage = function(message) { $('#messageList').prepend($('<li>').text(message.data)); }; webSocket.onerror = function() { alert('エラーが発生しました。'); }; }); $('#disconnectButton').click(function() { webSocket.close(); webSocket = null; $('#roomName').prop('disabled', false); $('#connectButton').prop('disabled', false); $('#disconnectButton').prop('disabled', true); }); $('#sendButton').click(function() { if (!webSocket) { alert('未接続です。'); return; } webSocket.send($('#message').val()); }); }); </script> </body> </html>
STOMPの時のほうが、いろいろシンプルに書けるので、ブラウザがクライアントならば、STOMPを使わない理由はないかなと思っています。
Spring Boot
でSTOMP Over WebSocket
を使って、試しにチャットっぽいものを作ってみました。
同じルームに接続しているクライアントに、同じメッセージを配信するだけならば、Controller
の定義はいりません。
WebSocket
のConfig
として、エンドポイントとSimpleBroker
でハンドリングする宛先を定義するだけです。
package com.example; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @EnableWebSocketMessageBroker @Configuration public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/endpoint"); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); } }
画面側では、配信と購読で同じ宛先を指定しておきます。自分で送ったものも受け取ることになります。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>チャット</title> <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap.min.css" /> <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css" /> </head> <body> <div class="container"> <h2>チャット</h2> <div class="form-horizontal"> <div class="form-group"> <label for="roomName" class="col-sm-2 control-label">ルーム</label> <div class="col-sm-2"> <input id="roomName" type="text" class="form-control" value="example" /> </div> <div class="col-sm-3"> <button id="connectButton" type="button" class="btn btn-default">接続</button> <button id="disconnectButton" class="btn btn-default">切断</button> </div> </div> <div class="form-group"> <label for="message" class="col-sm-2 control-label">メッセージ</label> <div class="col-sm-4"> <input id="message" type="text" class="form-control" /> </div> <div class="col-sm-2"> <button id="sendButton" type="button" class="btn btn-default">送信</button> </div> </div> <div class="row"> <div class="col-sm-4 col-sm-offset-2"> <ul id="messageList" class="list-unstyled"> </ul> </div> </div> </div> </div> <script src="/webjars/jquery/1.12.4/jquery.min.js"></script> <script src="/webjars/stomp-websocket/2.3.3-1/stomp.js"></script> <script src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script> $(function() { var endpoint = 'ws://' + location.host + '/endpoint'; var subscribePrefix = '/topic/'; var stompClient = null; $('#connectButton').click(function() { $("#messageList").empty(); stompClient = Stomp.over(new WebSocket(endpoint)); stompClient.connect({}, function() { stompClient.subscribe( subscribePrefix + $('#roomName').val(), function(message) { $('#messageList').prepend($('<li>').text(message.body)); }); $('#roomName').prop('disabled', true); $('#connectButton').prop('disabled', true); $('#disconnectButton').prop('disabled', false); }); }); $('#disconnectButton').click(function() { stompClient.disconnect(); stompClient = null; $('#roomName').prop('disabled', false); $('#connectButton').prop('disabled', false); $('#disconnectButton').prop('disabled', true); }); $('#sendButton').click(function() { if (!stompClient) { alert('未接続です。'); return; } stompClient.send(subscribePrefix + $('#roomName').val(), {}, $('#message').val()); }); }); </script> </body> </html>
クライアント間でメッセージをやり取りするといったことが、少しのコードで簡単に実現できますね。