Oracle 結合演算子(+) は奨励しない書き方だったと思いだす。。

久しぶりに Oracle 使用のプロジェクトで、
SQLで、WHERE句で結合演算子(+) を書いてるのを見かけた。。。

あれ!、たしか Oracle 12c で奨励されなくなったのでは?!。。。

https://docs.oracle.com/database/121/SQLRF/queries006.htm#SQLRF30046

引用すると以下のように書いてあるし。。
Oracle recommends that you use the FROM clause OUTER JOIN syntax rather than the Oracle join operator.
Outer join queries that use the Oracle join operator (+) are subject to the following rules and restrictions,
which do not apply to the FROM clause OUTER JOIN

FROM 句で
LEFT OUTER JOIN ~ ON ~
あるいは、
LEFT JOIN ~ ON ~

の方が、読みやすいし、
複数 (+)= を書いて想定外の結果を得るよりも
こちらの JOINを記述する方が良いと思う。

Bootstrap Datepicker カレンダーに、日本の祝日をマークする

元々、10年程前に Java で日本の祝日を求めるものを作って公開したのだが、
数年前に JavaScript 版も作っていた。jQuery - UIdatepicker で祝日をマークする為だった。

JavaScript 版も、バージョン 1.4 では、JHoliday.descriptionDate(date) にバグがあったので、
1.5 で修正し公開した。

Java祝日計算 プロジェクト日本語トップページ - OSDN


これを機に、Bootstrap の datepicker も対応してみる。
1.5 として配布した jholiday.js はそのままで変更はない。
以下のように、日本の祝日の日付文字色を赤くして、ツールチップで祝日説明をする。
f:id:posturan:20190611223719j:plain

JavaScript 版 1.5 をダウンロードして、他のJS、CSSCDNサイトで書くと、、

<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"  rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/css/bootstrap-datepicker.css"  rel="stylesheet" />
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://momentjs.com/downloads/moment-with-locales.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/js/bootstrap-datepicker.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/locales/bootstrap-datepicker.ja.min.js"></script>
<script src="jholiday.js"></script>

Bootstrap datepicker にする input タグ

<div class="col-md-2">
   <input type="text" class="form-control" id="sample">
</div>

CSS

.datepicker-days th.dow:first-child, .datepicker-days td:first-child{
    color: #ff0000;
}
.datepicker-days th.dow:last-child, .datepicker-days td:last-child{
    color: #0000ff;
}
.datepicker-days thead{
   border-bottom: 1px solid #cccccc;
}
.datepicker-days .holiday{
    color: #ff0000;
}

Bootstrap datepicker の指定

$('#sample').datepicker({
    format: 'yyyy/mm/dd',
    language:'ja',
    todayHighlight: true,
    enableOnReadonly: true,
    templates: {
       leftArrow: '<',
       rightArrow: '>'
    },
    beforeShowDay: function(date){
        if (JHoliday.isHolidayDate(date)==1){
           return {
             classes: 'holiday', tooltip:JHoliday.descriptionDate(date)
          };
        }
    },
});

beforeShowDay オプションでカレンダー日付表示前の
カレンダー表示日すべてが、Date型オブジェクトで関数を実行で戻り値に、
日付表示の class属性などを指定できる。
詳細は、↓
https://bootstrap-datepicker.readthedocs.io/en/latest/options.html#beforeshowday
これを利用して、JavaScript 版祝日計算で、祝日判定と descriptionDate で
祝日名称を取得して、ツールチップ表示させるようにする。

後日、このHTMLサンプルを JavaScript 版 1.51 として配布しようと思う。
jholiday.js は、1.5 のまま変更なしで、サンプル配布として追加するだけなので、1.51 である。

フォルダ指定のアップロード

HTML5 input の webkitdirectory 付与は、Google chrome と Fire Fox しか今のところ使えないが、、
Wicket でこれを受信するのは、FileUploadField コンポーネントの getFileUploads() で
List<FileUpload> を受け取ることになる。

<input wicket:id="uploadfile" id="uploadfile" type="file" webkitdirectory>

Wikcet

final FileUploadField fileuploadField = new FileUploadField("uploadfile");
queue(fileuploadField);
queue(new Button("submit").add(AjaxFormSubmitBehavior.onSubmit("click", t->{
   fileuploadField.getFileUploads().stream().forEach(f->{
      // f.getClientFileName());
      // f.getSize()  byte
      // Bytes.bytes(f.getSize()).toString()  KByte
      // f.getInputStream()
   });
})));

フォルダを選択した時の change イベントで JavaScript 上では以下のように
情報を読み取る。

$(function(){
   $("#uploadfile").change(function(ev) {
      for(var i=0; i < ev.target.files.length; i++){
         var file = ev.target.files[i];
         console.log(file);
         // ディレクトリの相対パス
         var path = file.webkitRelativePath;
         console.log([" + path + "]");
      }
   });
});

twitter/Typeahead.js を Wicket の AJAX として Autocomplete の振るまいにする

WicketAutocomplete といえば、jQuery UI を利用したものが慣れ親しんでものであった。
最近よく使われる Bootstrap デザインを適用しても使えないわけではない、
CSSスタイルシートを合わせて書いていけば良いのだが、
Bootstrap と jQuery UI を併用が嫌で jQuery UI をやめたくなる。
datepicker も Bootstrap向けが存在するわけだし、
Draggable / Drop これさえ Bootstrap でできれば、
jQuery UI を完全にやめれるのではないか?

前置きはこのくらいで、本題!
typeahead.js/jquery_typeahead.md at master · twitter/typeahead.js · GitHub

twitter/typeahead.js が Bootstrap デザインと相性が良いようだ。

まず、twitter/Typeahead.js のリモートからマッチ候補を受信する書き方を把握しておく。。
JSソースとしては、
<input type="text" id="typeahead"> に対して

$('input#typeahead').typeahead({
   highlight: true,
   minLength: 1
}, {
   //name : 'states',
   displayKey: 'display',
   limit: 7,
   source : new Bloodhound({
      datumTokenizer: function(datum){
         return Bloodhound.tokenizers.whitespace(datum.display);
      },
      queryTokenizer: Bloodhound.tokenizers.whitespace,
      remote:{
         wildcard: '%QUERY',
         url: 'http://xxxxxxxxxxxxx?query=%QUERY',
         transform: function(response){
            return $.map(response, function(item){
               return { display:item.value, id:item.id };
            });
         },
      }
   })
}).on('typeahead:select', function(ev, item){
     console.log('選択された id = ' + item.id);
     console.log('選択された 表示文字列 = ' + item.display);
}).on('change', function(ev){
     console.log($(this).val());
});

このようなJSソースをリソースとして用意するのではなく、
Wicket TextField の振るまい Behavior として
AJAX通信→ マッチ候補リスト表示させるようにするのです。

CSSは、以下に適当なものがあったので、
https://github.com/bassjobsen/typeahead.js-bootstrap-css
これを参考に以下のように用意します。

span.twitter-typeahead .tt-menu,
span.twitter-typeahead .tt-dropdown-menu {
  cursor: pointer;
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 1000;
  display: none;
  float: left;
  min-width: 100%;

  padding: 5px 0;
  margin: 2px 0 0;
  list-style: none;
  font-size: 14px;
  text-align: left;
  background-color: #ffffff;
  border: 1px solid #cccccc;
  border: 1px solid rgba(0, 0, 0, 0.15);
  border-radius: 4px;
  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
  background-clip: padding-box;
}
span.twitter-typeahead .tt-suggestion {
  display: block;
  padding: 3px 20px;
  clear: both;
  font-weight: normal;
  line-height: 1.42857143;
  color: #333333;
  white-space: nowrap;
}
span.twitter-typeahead .tt-suggestion.tt-cursor,
span.twitter-typeahead .tt-suggestion:hover,
span.twitter-typeahead .tt-suggestion:focus {
  color: #ffffff;
  text-decoration: none;
  outline: 0;
  background-color: #337ab7;
}
.input-group.input-group-lg span.twitter-typeahead .form-control {
  height: 46px;
  padding: 10px 16px;
  font-size: 18px;
  line-height: 1.3333333;
  border-radius: 6px;
}
.input-group.input-group-sm span.twitter-typeahead .form-control {
  height: 30px;
  padding: 5px 10px;
  font-size: 12px;
  line-height: 1.5;
  border-radius: 3px;
}
span.twitter-typeahead {
  width: 100%;
}
.input-group span.twitter-typeahead {
  display: block !important;
  height: 34px;
}
.input-group span.twitter-typeahead .tt-menu,
.input-group span.twitter-typeahead .tt-dropdown-menu {
  top: 32px !important;
}
.input-group span.twitter-typeahead:not(:first-child):not(:last-child) .form-control {
  border-radius: 0;
}
.input-group span.twitter-typeahead:first-child .form-control {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
.input-group span.twitter-typeahead:last-child .form-control {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
}
.input-group.input-group-sm span.twitter-typeahead {
  height: 30px;
}
.input-group.input-group-sm span.twitter-typeahead .tt-menu,
.input-group.input-group-sm span.twitter-typeahead .tt-dropdown-menu {
  top: 30px !important;
}
.input-group.input-group-lg span.twitter-typeahead {
  height: 46px;
}
.input-group.input-group-lg span.twitter-typeahead .tt-menu,
.input-group.input-group-lg span.twitter-typeahead .tt-dropdown-menu {
  top: 46px !important;
}

Wicket の TextField を継承
キー入力イベント後のAJAX通信で返す JSON は、Goole gson で生成しています。

import java.util.List;
import java.util.Optional;
import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.IModel;
import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.handler.TextRequestHandler;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
 * Twitter Boostrap Typeahead TextField.
 */
public abstract class AjaxTypeahead<T extends Typeahead> extends TextField<String>{
   private AbstractDefaultAjaxBehavior queryAjaxBehavior;
   private List<Typeahead> choicelist;

   public AjaxTypeahead(String id, IModel<String> model){
      super(id, model);
      Gson gson = new GsonBuilder().serializeNulls().create();
      queryAjaxBehavior = new AbstractDefaultAjaxBehavior(){
         @Override
         protected void respond(AjaxRequestTarget target){
            IRequestParameters p = getRequestCycle().getRequest().getQueryParameters();
            Optional.ofNullable(p.getParameterValue("query").toString()).ifPresent(s->{
               choicelist = getChoices(s.trim());
               getComponent().getRequestCycle()
.replaceAllRequestHandlers(new TextRequestHandler("application/json", "UTF-8", gson.toJson(choicelist)));
            });
            Optional.ofNullable(p.getParameterValue("selected").toString()).ifPresent(s->{
               String id = p.getParameterValue("id").toString();
               for(Typeahead t:choicelist){
                  if (t.id.equals(id)){
                     onSelect(target, t);
                     break;
                  }
               }
            });
            Optional.ofNullable(p.getParameterValue("change").toString()).ifPresent(s->{
               String display = Optional.ofNullable(p.getParameterValue("display").toString())
                           .orElse("").trim();
               onChange(target, display);
            });
            getRequestCycle().getResponse().close();
         }
         @Override
         protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
            super.updateAjaxAttributes(attributes);
            attributes.setDataType("json");
            attributes.setWicketAjaxResponse(false);
         }
      };
      add(queryAjaxBehavior);
   }

   @Override
   protected void onAfterRender(){
      super.onAfterRender();
      getResponse().write("<script type=\"text/javascript\">");
      getResponse().write(" $(\"#" + this.getMarkupId(true) + "\").typeahead({highlight:true,minLength:1},{");
      getResponse().write("displayKey: 'display',");
      getResponse().write("limit: 7,");
      getResponse().write("source : new Bloodhound({");
      getResponse().write("datumTokenizer: function(datum){");
      getResponse().write("return Bloodhound.tokenizers.whitespace(datum.display);");
      getResponse().write("},");
      getResponse().write("queryTokenizer: Bloodhound.tokenizers.whitespace,");
      getResponse().write("remote:{");
      getResponse().write("wildcard: '%QUERY',");
      getResponse().write("url: '" + queryAjaxBehavior.getCallbackUrl() + "&query=%QUERY',");
      getResponse().write("transform: function(r){");
      getResponse().write("return $.map(r, function(t){");
      getResponse().write("return { display:t.value, id:t.id };");
      getResponse().write("});}}})");
      getResponse().write("}).on('typeahead:select', function(ev, item){");
      getResponse().write("var url = '" + queryAjaxBehavior.getCallbackUrl() + "' + '&selected=&id=' + item.id + '&display=' + item.display;");
      getResponse().write("Wicket.Ajax.get({ u: url });");
      getResponse().write("}).on('change', function(ev){");
      getResponse().write("var url = '" + queryAjaxBehavior.getCallbackUrl() + "' + '&change=&display=' + $(this).val();");
      getResponse().write("Wicket.Ajax.get({ u: url });");
      getResponse().write("});");
      getResponse().write("</script>");
   }

   /** 入力文字→候補リスト  */
   protected abstract List<Typeahead> getChoices(String input);

   /**
    * 選択イベント捕捉.
    * @param target AjaxRequestTarget
    * @param id
    * @param dislay
    */
   protected void onSelect(AjaxRequestTarget target, Typeahead typeahead){
   }
   /** 変更イベント捕捉    */
   protected void onChange(AjaxRequestTarget target, String dislay){
   }
}

表示用の素材、Typeahead は、id と プルダウン表示及び入力文字列をSerializableである。
この Typeahead を継承するかそのまま使用する。

import java.io.Serializable;

/**
 * twitter/Typeahead.js element.
 */
public class Typeahead implements Serializable{
   public String id;
   public String value;

   public Typeahead(){}

   public void setValue(String value){
      this.value = value;
   }
   public void setId(String id){
      this.id = id;
   }
}

使用する例: Example

AjaxTypeahead<Typeahead> simpleField = new AjaxTypeahead<>("name", new Model<>()){
   @Override
   protected List<Typeahead> getChoices(String input){
      // 入力が空 Enter では、空リストを送る。
      if (Strings.isEmpty(input)) return Collections.emptyList();
      // TODO 入力 input にマッチするリストを返す。
      return list;
   }
   @Override
   protected void onSelect(AjaxRequestTarget target, String id, String dislay){
      // TODO 選択時の処理、選択した要素の id と 表示文字列 display を受信
   }
   @Override
   protected void onChange(AjaxRequestTarget target,  String dislay){
      // TODO change イベントの処理、変更後の表示文字列 display を受信
   }
};
queue(simpleField);

あくまでも、TextField の継承であり、
フォーム送信で受け取る Wicket の Model オブジェクトは、String である。

Bootstrap の datepicker

メモ、
以下からダウンロード
https://github.com/uxsolutions/bootstrap-datepicker

HTML head 読込み指定
曜日を日本語表示の為には、配布されてる中の bootstrap-datepicker.ja.min.js を使う。

<link href="dist/css/bootstrap-datepicker.css"  rel="stylesheet" />
<script src="dist/js/bootstrap-datepicker.js"></script>
<script src="dist/locales/bootstrap-datepicker.ja.min.js"></script>

CSS・・・自由に。

.datepicker-days th.dow:first-child,
.datepicker-days td:first-child {
   color: #f00;
}
.datepicker-days th.dow:last-child,
.datepicker-days td:last-child {
    color: #00f;
}
.datepicker-days thead{
   border-bottom: 1px solid #cccccc;
}
.datepicker-title{
   background-color: #ffbb11;
}

datepicker 指定

$('#date_sample').datepicker({
    format: 'yyyy/mm/dd',
    language:'ja',
    todayHighlight: true,
    templates: {
        leftArrow: '<',
        rightArrow: '>'
    },
}).on('change', function(){
     // TODO $(this).val()
});

f:id:posturan:20190601154918j:plain

タイトルを付ける Option

$('#date_sample').datepicker({
    format: 'yyyy/mm/dd',
    language:'ja',
    todayHighlight: true,
    title: '誕生日',
    templates: {
        leftArrow: '<',
        rightArrow: '>'
    },
}).on('change', function(){
     // TODO $(this).val()
});

f:id:posturan:20190601155247j:plain

他にもオプション、
曜日の始まり指定

weekStart: 1, // 0=Sunday

曜日によるハイライト表示

daysOfWeekHighlighted: [0],

などがある。

画像バイナリデータからMIMEタイプを判定する。

Java で、画像バイナリデータ byte[] の状態のデータから image/jpeg などのタイプを調べます。
画像 File であれば、java.nio.file.Files probeContentType(Path) を使えば良いのですが、
バイナリデータの状態から検査したい場合が稀にあるでしょう。

データの先頭8バイトをチェックすれば済みます。
ついでに画像の width と height も取得します。

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;

public static Map<String, String> getImageInfo(byte[] b) throws IOException{
   Map<String, String> r = new HashMap<>();
   try(ByteArrayInputStream bin = new ByteArrayInputStream(b)){
      BufferedImage bimg = ImageIO.read(bin);
      r.put("width", Integer.toString(bimg.getWidth()));
      r.put("height", Integer.toString(bimg.getHeight()));
   }
   StringBuilder sb = new StringBuilder();
   for(int i=0;i < 8;i++){
      sb.append(String.format("%02x", b[i]));
      if (sb.toString().equals("ffd8")){
         r.put("type", "image/jpeg");
         break;
      }
      if (sb.toString().equals("424d")){
         r.put("type", "image/bmp");
         break;
      }
      if (sb.toString().equals("47494638")){
         r.put("type", "image/gif");
         break;
      }
      if (sb.toString().equals("49492a00")){
         r.put("type", "image/tiff");
         break;
     }
      if (sb.toString().equals("89504e470d0a1a0a")){
         r.put("type", "image/png");
      }
   }
   return r;
}

File を指定しても同じメソッドをcall するようにラップしておくと良いでしょう。

public static Map<String, String> getImageInfo(File file) throws IOException{
   try(InputStream in = new FileInputStream(file);
        ByteArrayOutputStream out = new ByteArrayOutputStream()){
      in.transferTo(out);
      out.flush();
      return getImageInfo(out.toByteArray());
   }
}

Wicket RequestCycle から getResponse した OutputStream で注意すべき点

Wicket 8.2.0 までは、Wicket RequestCycle
即ち、WebPage上で getRequestCycle().getResponse()
で取得する org.apache.wicket.request.Responseから
取得する OutputStream に書き込むことで、
HTML でないものをレスポンス応答することに問題は発生しなかった。
しかし、Wicket 8.3.0 以上のバージョンでは、
以下のように、JPEG画像を返す WebPage として構築すると、、
  flower.jpg を resouce上に置いたとして、

String classesPath = ((WebApplication)getApplication()).getServletContext().getRealPath("WEB-INF/classes");
File imagefile = new File(classesPath + "/img/flower.jpg");

OutputStream を RequestCycle から getResponse を介して取得して、

WebResponse webresponse = (WebResponse)getRequestCycle().getResponse();
webresponse.setContentType("image/jpeg");
try(InputStream in = new FileInputStream(imagefile)){
       OutputStream out = getRequestCycle().getResponse().getOutputStream();
       in.transferTo(out);
       out.flush();
}catch(Exception ex){
       logger.error(ex.getMessage(), ex);
}

このように画像を送ると、
重大: サーブレット [default] のServlet.service()が例外を投げました
java.lang.IllegalStateException: レスポンスがコミットされた後でsendError()を呼び出すことはできません

となってしまう。
Wicket 8.3.0 以降 WebFilter の処理が変わったことを示しており、
このように直接 RequestCycle から取得する OutputStream に出力したのでは順序が遅いようだ。
解決策、、、
Wicket 8.3.0~ 以降では、、
RequestCycle の scheduleRequestHandlerAfterCurrent を使用して
org.apache.wicket.request.IRequestHandler の
  respond(IRequestCycle requestCycle) 
ラムダ式で、IRequestCycle 即ち、RequestCycle から取得する OutputStreamで出力するようにする。

上の imagefile 出力は、以下のように書く。

getRequestCycle().scheduleRequestHandlerAfterCurrent(c->{
   WebResponse ws = (WebResponse)c.getResponse();
   ws.setContentType("image/jpeg");
   try(InputStream in = new FileInputStream(imagefile)){
      OutputStream out = c.getResponse().getOutputStream();
      in.transferTo(out);
      out.flush();
   }catch(Exception ex){
      logger.error(ex.getMessage(), ex);
   }
});

Wicket 8.3.0 から、こう書かないとダメだ。