Wicket8 の Tomcat 8 で Websocket に苦戦中

2017-6-25 に回答

oboe2uran.hatenablog.com


先日、Wicket 8 の websocket - Oboe吹きプログラマの黙示録 で書いたが、改めて取り掛かった。

Wicket の native-Websocket を使おうと、Tomcat 8.0.43 で起動できない。
Websocket にそんなに魅力を感じてるわけでもなく別の手段を選ぶことが多いのだが。。。調べてみる。

    <dependency>
      <groupId>org.apache.wicket</groupId>
      <artifactId>wicket-native-websocket-tomcat</artifactId>
      <version>8.0.0-M3</version>
   </dependency>

   <dependency>
     <groupId>org.apache.wicket</groupId>
      <artifactId>wicket-native-websocket-javax</artifactId>
      <version>8.0.0-M5</version>
   </dependency>

もう、この時点、wicket-native-websocket-tomcat 8.0.0-M3 と名前になっていても、中は、Tomcat7 の
org.apache.wicket.protocol.ws.tomcat7.Tomcat7WebSocketFilter なので、うまく動かないのはあたりまえだ。


たくさんエラー出て起動できない

Caused by: java.lang.ClassNotFoundException: org.apache.coyote.http11.upgrade.UpgradeInbound
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1333)
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1167)
	... 20 more

重大: Failed to destroy end point associated with ProtocolHandler ["http-nio-8080"]
java.lang.NullPointerException
	at org.apache.tomcat.util.net.NioEndpoint.releaseCaches(NioEndpoint.java:315)
	at org.apache.tomcat.util.net.NioEndpoint.unbind(NioEndpoint.java:491)
	at org.apache.tomcat.util.net.AbstractEndpoint.destroy(AbstractEndpoint.java:883)
  :
  :

4 29, 2017 2:28:23 午後 org.apache.coyote.AbstractProtocol destroy
情報: Destroying ProtocolHandler ["ajp-nio-8009"]
4 29, 2017 2:28:23 午後 org.apache.coyote.AbstractProtocol destroy
重大: Failed to destroy end point associated with ProtocolHandler ["ajp-nio-8009"]
java.lang.NullPointerException
	at org.apache.tomcat.util.net.NioEndpoint.releaseCaches(NioEndpoint.java:315)
	at org.apache.tomcat.util.net.NioEndpoint.unbind(NioEndpoint.java:491)
	at org.apache.tomcat.util.net.AbstractEndpoint.destroy(AbstractEndpoint.java:883)

ClassNotFoundException: org.apache.coyote.http11.upgrade.UpgradeInbound

だから、7系の Tomcat coyote を持ってくる。

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-coyote</artifactId>
    <version>7.0.73</version>
</dependency>

それでも、まだ次のエラーになる。

重大: フィルタ WebSocketFilter の起動中の例外です
java.lang.NoSuchFieldError: ISO_8859_1
	at org.apache.wicket.protocol.ws.tomcat7.Tomcat7WebSocketFilter.<clinit>(Tomcat7WebSocketFilter.java:43)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at java.lang.Class.newInstance(Class.java:442)
	at org.apache.catalina.core.DefaultInstanceManager.newInstance(DefaultInstanceManager.java:119)
	at org.apache.catalina.core.ApplicationFilterConfig.getFilter(ApplicationFilterConfig.java:258)
	at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:105)
	at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:4700)
	at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5340)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:145)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1408)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1398)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

Tomcat7WebSocketFilter が、エンコードの文字セット定義を org.apache.tomcat.util.buf.B2CConverter から持ってきて
初期化しようとして失敗している。

Tomcat8 に、無理やり、7系で用意されたライブラリを組み合わせて起動しようとしていること自体がアホだと
思いつつ、なんとか手を掛けないで、、、と甘い考えがだめだった。

ならば、Wicket が配布した Tomcat7WebSocketFilter を参考に、Tomcat8 用の WebSocketFilter を作ろう!!

B2CConverterも自分で用意して、

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>7.0.77</version>
</dependency>

文字コード系は、対応をする。これで起動はするのだが、実際の Websocket エンドポイントを割り当てがうまくいくかと
いうと、そうはいかない!!

org.apache.catalina.connector.RequestFacade の doUpgrade をなんとかしなくてはならない!!

まだまだ、苦戦しそう。。

input テキストフィールドでカーソルの位置を指定する。

HTML  input テキストフィールドで任意の範囲を選択するには、

<input type="text" value="1234567890">

jQuery で、focus して、setSelectionRange( 開始位置、終了位置 );

    $('input').focus().get(0).setSelectionRange(3, 6);

f:id:posturan:20170425223953j:plain

setSelectionRange で指定する値は、0始まりで数える。

この setSelectionRange を使って、カーソルの位置を任意の位置に指定することができる。
→ 開始位置 と 終了位置を同じにすれば、その位置にカーソルを持ってくることができる。

    $('input').focus().get(0).setSelectionRange(3, 3);

これは、上の input であれば、3と4の間にカーソルがくる。
以下でも同じくカーソルが3と4の間にくるが、

$('input').get(0).selectionStart = 3;
$('input').get(0).selectionEnd = 3;

selectionEnd を 6 で指定して

$('input').get(0).selectionStart = 3;
$('input').get(0).selectionEnd = 6;

これで、範囲選択になるのかというとそうではない。setSelectionRange( 開始位置、終了位置 ); で同じ値をセット
するというので、カーソルの位置を任意の位置に指定すると覚えた方が良い気がする。

現在のカーソル位置に、任意の文字を挿入したいなら、

var pos =  $('input').get(0).selectionStart;
$('input').val( $('input').val().substr(0, pos) + '-' + $('input').val().substr(pos));

3の後にカーソルを充てて、
f:id:posturan:20170425230604j:plain

原始的なHTTPクライアントサンプル

久々に HTTPクライアントプログラムを書く。
以前は https://hc.apache.org/ を使うことばかりしていた。chunk発生を考慮すれば当然だった。

でも、限られたネットワークで軽量に動かしたく前にも書いたかもしれない 原始的なHTTP クライアント
(=他のライブラリを使わない、java.net.HttpURLConnection でなんとかする)のサンプルを改めて書き直した。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * Sample.
 */
public class SampleClient {
   /**
    * @param path URLのパス
    * @param parameters POST送信パラメータ、key-value
    */
   public String execute(String path, Map<String, String> parameters){
      String result = null;
      try{
         URL url = new URL(path);
         HttpURLConnection uc = (HttpURLConnection)url.openConnection();
         uc = (HttpURLConnection)url.openConnection();
         /* HTTPリクエストヘッダの設定 */
         uc.setDoOutput(true);             // こちらからのデータ送信を可能とする
         uc.setReadTimeout(0);             // 読み取りタイムアウト値をミリ秒単位で設定(0は無限)
         uc.setRequestMethod("POST");      // URL 要求のメソッドを設定
         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();

         // 戻り値取得
         System.out.println("ContentType: " + uc.getContentType());

         Map<String,List<String>> hmap = uc.getHeaderFields();
         List<String> hlist = hmap.get("Content-Language");
         if (hlist != null && hlist.size()==1){
            System.out.println("ContentLanguage: " + hlist.get(0) );
         }
         hlist = hmap.get("Date");
         if (hlist != null && hlist.size()==1){
            System.out.println("ContentDate: " + hlist.get(0) );
         }
         hlist = hmap.get("Content-Length");
         if (hlist != null && hlist.size()==1){
            System.out.println("ContentLength: " + hlist.get(0) );
         }
         System.out.println("ResponseCode: " + uc.getResponseCode() );
         System.out.println("ResponseMessage: " + uc.getResponseMessage() );

         try(BufferedReader in = new BufferedReader(new InputStreamReader(uc.getInputStream(), "utf8"))   ){
            StringBuffer out = new StringBuffer();
            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);
         }finally{
         }
      }catch(Exception e){
         e.printStackTrace();
      }finally{
      }
      return result;
   }
}

あえて、途中、標準出力で状況を出している。例外もprintStackTrace で出すサンプル。

Wicket ModalWindow full screen

Wicket ModalWindow full screen と言うと、ちょっと言い過ぎだけど、つまりブラウザで開いたページ表示一杯の
幅、高さで ModalWindow しかも caption なしの ModalWindow に見えない ModalWindow を出したくなった。

昔、captionが無い~フレーム枠が存在しないModalWindow を出したことがある。
ドラッグさせないWicketのModalWindow - Oboe吹きプログラマの黙示録

これをヒントに、更なる展開としてModalWindow に見えないPageの上を別のPageで重ねたように表示する Panel で
CLOSEボタンを配置して元のページに戻ったときに Panel で入力した情報を受け取るのが目的です。
Page遷移させたくないのです。

PageのHTML bodyタグの直下に ModalWindow する div を置きます。

<body>
<div wicket:id="modal"></div>
<a wicket:id="test"  href="#">open</a>

Panel は、必ず CLOSE する為のボタンを置きます。

<wicket:panel>
<form wicket:id="form">
<button wicket:id="close" type="button">CLOSE</button>
</form>
</wicket:panel>

Page の Java

ModalWindow window = new ModalWindow("modal");
queue(window);
queue(AjaxLink.onClick("test", t->{
   window.setContent(new ModalPanel(window.getContentId()));
   window.show(t);
}));

Panel

queue(new Form<Void>("form"));
queue(AjaxButton.onSubmit("close", (b, t)->ModalWindow.closeCurrent(t)));

問題は、CSS です。wicket-modal クラスでとにかく、position 指定でフル表示を指定し、
wicket-mask-dark : 背景表示を強制的に display : none
モーダルのコンテンツの高さを 100% と指定したいところですが、そうでなくて、
最近導入された、CSS3 の Viewports units で画面高さの比率で指定します。height: 100vh とすると
どうしても Wicket が生成する Modal コンテンツ枠が僅かに引っ掛かってブラウザにスクロールバーが
生じてしまいます。

.wicket-modal{
   top: 0 !important;
   left: 0px !important;
   width: 100% !important;
   height: 100% !important;
   position: absolute !important;
   visibility: visible !important;
}
.w_caption, div.w_top_1, div.w_topLeft, div.w_topRight, div.w_bottom_1{
   display: none;
}
.wicket-mask-dark{
   display: none !important;
}
div.w_left, div.w_right
, div.wicket-modal div.w_content_1
, div.wicket-modal div.w_right_1
, div.wicket-modal div.w_left_1
, div.w_content_1, div.w_content_2, div.w_content_3, div.w_window_3 {
   margin: 0;
   background-image: none;
   _background-image: none;
   border: none:
   display: none;
   height: 99vh; !important;
}

だから、99vh にします。それでもわずかに線が出るのですが、ここまでできれば良しとします。

window.close() の実行

JavaScript で開いている自身のページのブラウザWindow は、セキュリティのために閉じられないように
なっているが、aタグ target="_blank" 開いた先での window.close() は効いてくる。

結構昔から、いろんなことが試されていた。
stackoverflow.com

jQuery UI の datepicker trigger を画像でなくてフォントアイコンで指定

jQuery UI の datepicker を使ってるWebサイトで、わざわざアイコンのボタンによる picker 呼出しをするデザインがなんか釈然としない。

https://jqueryui.com/datepicker/#icon-trigger

にあるようなカレンダーアイコンをクリックで呼び出すもの。これより入力フィールドをクリック、focusされた時に呼び出すのでわざわざアイコンがあるのが
釈然としない。

jqueryui.com サイトにあるサンプルは、アイコン画像も gif でありこのように画像サイズを気にしながら準備するのも煩わしい。

このアイコンをGIFでなくて、フォントアイコンで描画させてサイズ調整しても描画が乱れないようにできないだろうかと考えて試してみる。

まず。jQuery UI datepicker のデモページに書いてあるコードは

$( "#datepicker" ).datepicker({
      showOn: "button",
      buttonImage: "images/calendar.gif",
      buttonImageOnly: true,
      buttonText: "Select date"
});

と書いてあって、これで input タグの後ろに自動的に次の img タグが描画される。

<img class="ui-datepicker-trigger" src="images/calendar.gif" alt="Select date" title="Select date">

この img タグ生成させる代わりに、buttonタグ生成にさせたい。そこで、buttonImage と buttonImageOnly を指定しないようにする。

$( "#datepicker" ).datepicker({
      prevText:"前月", nextText:"翌月",
      changeMonth: true, monthNamesShort: ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],
      showOn: "button",
      buttonText: "Select date"
});

この段階で、
f:id:posturan:20170415183235j:plain
ということになる。

フォントアイコンとして、Fontello - icon fonts generator から必要なフォントファイルをダウンロードして
次のように CSS を用意する。

@font-face{
	font-family: 'fontello';
	src: url('../../fontello/font/fontello.eot');
	src: url('../../fontello/font/fontello.eot#iefix') format('embedded-opentype'),
	     url('../../fontello/font/fontello.woff') format('woff'),
	     url('../../fontello/font/fontello.ttf') format('truetype'),
	     url('../../fontello/font/fontello.svg#fontello') format('svg');
	font-weight: normal;
	font-style: normal;
}
.ui-datepicker-trigger{
	font-family: "fontello";
	font-style: normal;
	font-weight: normal;
	font-size: 16[f:id:posturan:20170415184856j:plain]px;
	display: inline-block;
	text-decoration: inherit;
	cursor: pointer;
	color: #3456ff;
}

そして、datepicker設定の buttonText は、fontello の約束ではタグBODYに書くコードでアイコンを指定するので、

$(function(){
   $("#date").datepicker({
      prevText:"前月", nextText:"翌月",
      changeMonth: true, monthNamesShort: ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],
      showOn: "button",
      buttonText: "&#xe860;"
   }).datepicker("setDate", moment().format('YYYY/MM/DD'));
});

すると buttonタグとしてフォントアイコン埋め込みのボタンになる。 ( HTML-BODY に背景色指定 )

f:id:posturan:20170415184320j:plain

このままでもいいけど、どうせならボタンとしての形状でなくて、アイコン画像としてフラットな描画にしたいので、CSSで以下のように追加する

.ui-datepicker-trigger{
   font-family: "fontello";
   font-style: normal;
   font-weight: normal;
   font-size: 16px;
   display: inline-block;
   text-decoration: inherit;
   cursor: pointer;
   color: #3456ff;
   /* 追加 */
   background-color: rgba(0,0,0,0);
   border: none;
}

background-color と border を指定しなおしてしまう。透過させたいので、rgba で指定する。

f:id:posturan:20170415184856j:plain

このようになる。