« 2010年4月 | トップページ | 2010年6月 »

2010年5月

2010年5月28日 (金)

Hibernate search 導入

Hibernate search は 全文検索の機能を利用できるようにした JBoss Hibernate へのアドオン・コンポーネントである。全文検索機能へのアクセスには JBoss Hibernate の API とアノテーションを通じて行う[参考]。増田内科様が既に導入されているので,コードを使わせて頂いた。最初のインデックス作成にちょっと時間がかかるが,インデックスができてしまえば,後は全文検索が劇的に早くなる。インデックス作成は最初だけでよく,以降はデータ作成ごとに自動的に作成される。

 

約 30万件の ModuleModel の全文検索にかかった時間

元町皮ふ科方式Hibernate Search
初期インデックス作成3分50秒
検索にかかった時間4分30秒5 秒以下

Hibernatesearch

Hibernatesearch2

  1. Hibernate search 3.1.1GA をダウンロードする
  2. Hibernate search の以下のファイルを OpenDolphin の lib フォルダに入れてライブラリに加える。同じファイルを JBoss サーバの $JBOSS_HOME/common/lib にも入れて JBoss を再起動する。
    • hibernate-search-3.1.1.GA.jar
    • lucene-analyzers-2.4.1.jar
    • lucene-core-2.4.1.jar
  3. persistence.xml に以下の様にプロパティーを加える。下記の例では,/var/lucene/indexes にインデックスが作られる。
    <property name="hibernate.search.default.directory_provider"
    value="org.hibernate.search.store.FSDirectoryProvider"/>
    <property name="hibernate.search.default.indexBase" value="/var/lucene/indexes"/>
    
  4. Hibernate Search 用のアノテーションを加える

    infomodel/ModuleModel.java

    @Entity
    @Indexed(index="module")   // masuda
    @Table(name = "d_module")
    public class ModuleModel extends KarteEntryBean implements Stamp {
        
        private static final long serialVersionUID = -8781968977231876023L;;
    
        @Embedded
        private ModuleInfoBean moduleInfo;
        
        @Transient
        private IInfoModel model;
        
        @Lob
        // masuda
        @Field(index=Index.TOKENIZED)
        @FieldBridge(impl = ModuleModelBridge.class)
        @Analyzer(impl = CJKAnalyzer.class)
        // masuda
        @Column(nullable=false)
        private byte[] beanBytes;
    
  5. ModuleModel からインデックス用のキーワードを取り出すブリッジ

    infomodel/ModuleModelBridge.java

    package open.dolphin.infomodel;
    
    import java.beans.XMLDecoder;
    import java.io.BufferedInputStream;
    import java.io.ByteArrayInputStream;
    import org.hibernate.search.bridge.StringBridge;
    
    /**
     * ModuleModelのbeanBytesからテキストを取り出すブリッジ
     *
     * @author masuda, Masuda Naika
     */
    public class ModuleModelBridge implements StringBridge {
        @Override
        public String objectToString(Object object) {
    
            byte[] beanBytes = (byte[]) object;
            InfoModel im = (InfoModel) xmlDecode(beanBytes);
            String text = "";
    
            if (im instanceof ProgressCourse) {
                String xml = ((ProgressCourse) im).getFreeText();
                text = extractText(xml);
            } else {
                text = im.toString();
            }
    
            return text;
        }
    
        private String extractText(String xml) {
            StringBuilder sb = new StringBuilder();
            String head[] = xml.split("");
            for (String str : head) {
                String tail[] = str.split("");
                if (tail.length == 2) {
                    sb.append(tail[0].trim());
                }
            }
            return sb.toString();
        }
        private Object xmlDecode(byte[] bytes) {
    
            XMLDecoder d = new XMLDecoder(
                    new BufferedInputStream(
                    new ByteArrayInputStream(bytes)));
    
            return d.readObject();
        }
    }
    
  6. インデックス作成と検索の ejb

    ejb/RemoteDocumentPeekerServiceImpl.java

    /**
     * hibernate search のインデックスをクリアする
     */
    public void clearIndex() {
      FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em);
      fullTextEntityManager.purgeAll(ModuleModel.class);
    }
    
    /**
     * hibernate search のインデックスを作る - fromModuleId から toModuleId まで
     * @param fromModuleId
     * @param toModuleId
     */
    public void makeInitialIndex(Long fromModuleId, Long toModuleId) {
      FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em);
    
      List<ModuleModel> modules = getModuleModel(fromModuleId, toModuleId);
      for (ModuleModel mm : modules) {
        fullTextEntityManager.index(mm);
      }
    }
    
    /**
     * create and execute a search
     * @param text
     * @return
     */
    public List<PatientModel> getPatientTextSearch2(String text) {
    
      FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em);
    
      try {
        // create native Lucene query
        org.apache.lucene.queryParser.QueryParser parser = new QueryParser("beanBytes", new org.apache.lucene.analysis.cjk.CJKAnalyzer());
        org.apache.lucene.search.Query luceneQuery = parser.parse(text);
    
        // wrap Lucene query in a javax.persistence.Query
        javax.persistence.Query persistenceQuery = fullTextEntityManager.createFullTextQuery(luceneQuery, ModuleModel.class);
    
        // execute search
        List<ModuleModel> result = persistenceQuery.getResultList();
    
        // return result
        List<PatientModel> ret = new ArrayList();
        for (ModuleModel mm : result) {
          PatientModel pm = mm.getKarte().getPatient();
          if (!ret.contains(pm)) {
            ret.add(pm);
          }
        }
        return ret;
    
      } catch (ParseException ex) {
        ex.printStackTrace();
      }
      return null;
    }
    

    ejb/RemoteDocumentPeekerService.java

    public interface RemoteDocumentPeekerService {
     ・
     ・
        public void clearIndex();
        public void makeInitialIndex(Long fromModuleId, Long toModuleId);
        public List getPatientTextSearch2(String text);
    }
    
  7. Patient Search に組み込む

    plugin/PatientSearchImpl.java

    private void initComponents() {
     ・
     ・
      // HibernateSearchを使用するか masuda
      final JCheckBox cbUseHibernateSearch = view.getUseHibernateSearchCb();
      cbUseHibernateSearch.setSelected(preferences.getBoolean(HIBERNATE_SEARCH, false));
      cbUseHibernateSearch.addItemListener(new ItemListener() {
        @Override
        public void itemStateChanged(ItemEvent e) {
          preferences.putBoolean(HIBERNATE_SEARCH, cbUseHibernateSearch.isSelected());
        }
      });
      cbUseHibernateSearch.addMouseListener(new MouseAdapter() {
        @Override
        public void mousePressed(MouseEvent e) { maybePopup(e); }
        @Override
        public void mouseReleased(MouseEvent e) { maybePopup(e); }
        private void maybePopup(MouseEvent e) {
          if (e.isPopupTrigger() && cbUseHibernateSearch.isSelected() && e.isShiftDown()) {
            JPopupMenu popup = new JPopupMenu();
            JMenuItem mi = new JMenuItem("インデックス作成");
            popup.add(mi);
            mi.addActionListener(new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                makeInitialIndex();
              }
            });
            popup.show(e.getComponent(), e.getX(), e.getY());
          }
        }
      });
    
      final JToggleButton rb_karteSearch = view.getKarteSearchBtn();
      view.getUseHibernateSearchCb().setVisible(rb_karteSearch.isSelected());
      rb_karteSearch.addItemListener(new ItemListener() {
        @Override
        public void itemStateChanged(ItemEvent e) {
          boolean b = rb_karteSearch.isSelected();
          view.getUseHibernateSearchCb().setVisible(b);
        }
      });
    }
    // Hibernate Search関連 masuda
    private void makeInitialIndex() {
      app = ClientContext.getApplicationContext().getApplication();
      taskMonitor = ClientContext.getApplicationContext().getTaskMonitor();
    
      IndexTask task = new IndexTask(app);
      MyInputBlocker blocker = new MyInputBlocker(task, getContext().getGlassPane(), keyBlocker);
      getContext().getGlassPane().setText("インデックス作成は時間がかかります。");
      task.setInputBlocker(blocker);
    
      StatusMonitor bar = new StatusMonitor(task, taskMonitor, view.getProgressBar());
      ClientContext.getApplicationContext().getTaskService().execute(task);
    }
    
    /**
     * Hibernate Search の index を作成する
     */
    class IndexTask extends Task<Void, Void> {
      public IndexTask(Application app) {
        super(app);
      }
      @Override
      protected Void doInBackground() {
        logger.info("IndexTask started at " + new Date());
    
        // progress bar 設定
        String mess = "インデックス作成";
        String note = "索引を作成中( 0% 完了)";
        ProgressMonitor mon = new ProgressMonitor(view, mess, note, 0, 100);
        mon.setMillisToDecideToPopup(0); // この処理は絶対時間がかかるので,すぐ出す
        mon.setProgress(0);
        JDialog dialog = (JDialog) mon.getAccessibleContext().getAccessibleParent();
    
        // 索引作成開始
        DocumentPeekerDelegater dl = new DocumentPeekerDelegater();
    
        boolean hasNext = dl.makeInitialIndex(0L, 5000L); // 0 から 5000 づつインデックス作成
    
        while (hasNext) {
          // progress bar 表示
          int ratio = (int) (100*(dl.fromModuleId)/dl.maxModuleId);
          mon.setProgress(ratio);
          mon.setNote("索引を作成中(" + ratio + "%完了)");
          // キャンセルされた場合
          if (!dialog.isVisible()) break;
          hasNext = dl.makeInitialIndex();
        }
        mon.close();
        return null;
      }
      @Override
      protected void succeeded(Void v) {
        logger.info("IndexTask completed at " + new Date());
      }
      @Override
      protected void failed(Throwable cause) {
        logger.info("IndexTask failed at " + new Date());
        //logger.info(cause.getCause());
        //logger.info(cause.getMessage());
      }
      @Override
      protected void cancelled() {
        logger.info("IndexTask cancelled at " + new Date());
      }
    }
     ・
     ・
    class FindTask extends Task<Collection, Void> {
     ・
     ・
      protected Collection doInBackground() throws Exception {
        logger.debug("FindTask doInBackground");
        Preferences preferences = Preferences.userNodeForPackage(this.getClass());
        boolean inchiki = preferences.getBoolean(HIBERNATE_SEARCH, false);
     ・
     ・
        //pns -- 時間のかかるカルテ内検索
        //   カルテ内検索は時間がかかるので,moduleId で increaseStep ごと区切って検索
      } else if (!inchiki) {
     ・
     ・
      } else {
        // Hibernate Search を使ったカルテ全文検索
        // カルテ内検索をちょっとインチキする masuda
        DocumentPeekerDelegater dl = new DocumentPeekerDelegater();
        result = new ArrayList<PatientModel>();
        Collection pm = dl.getPatientOfKarte2(searchText);
        result.addAll(pm);
      }
     ・
     ・
    

2010年5月24日 (月)

スタンプ箱の保存・読み込み

スタンプ箱の内容を xml で保存・読み込みできるようにしたつもりだったが,スタンプ箱の内容(stampBytes)が保存できてなかった。どうしたらいいか悩んでいたら,なんと既に増田内科様がプログラムしてくださっていてびっくり。コード使わせていただきました。ありがとうございます。作りためた大切なスタンプのバックアップが取れるようになって安心度アップ。

Extramenu

Stampsave

Stampload

client/StampBoxPluginExtraMenu.java

client/StampBoxPlugin.java

 ・
 ・
/**
 * プログラムを開始する。
 */
public void start() {
 ・
 ・
//pns^ 特別メニューボタンを生成する
  extraBtn = new JButton();
  extraBtn.setContentAreaFilled(false);
  extraBtn.setIcon(GUIConst.ICON_GEAR_16);
  extraBtn.setToolTipText("特別メニュー");
  extraBtn.setFocusable(false);
  extraBtn.setPreferredSize(new java.awt.Dimension(16,16));
  extraBtn.addMouseListener(new StampBoxPluginExtraMenu(this));
//pns$
  //
  // レイアウトする
  //
  stampBoxPanel = new JPanel(new BorderLayout());
  stampBoxPanel.add(parentBox, BorderLayout.CENTER);
  JPanel cmdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
  cmdPanel.add(toolBtn);
  cmdPanel.add(publishBtn);
  cmdPanel.add(importBtn);
  cmdPanel.add(curBoxInfo);
//pns^
  JPanel utilPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
  utilPanel.add(extraBtn);
  utilPanel.add(lockBtn);
  JPanel cmdPanel2 = new JPanel(new BorderLayout());
  cmdPanel2.setPreferredSize(new java.awt.Dimension(38, 38));
  cmdPanel2.add(cmdPanel, BorderLayout.CENTER);
  cmdPanel2.add(utilPanel, BorderLayout.EAST);
// stampBoxPanel.add(cmdPanel, BorderLayout.NORTH);
  stampBoxPanel.add(cmdPanel2, BorderLayout.NORTH);
//pns$
 ・
 ・

2010年5月19日 (水)

JBoss AS 5.1.0.GA に乗り換え

OpenDolphin サーバを JBoss AS 4.2.3.GA から 5.1.0.GA にバージョンアップした。実運用して2週間ほど様子をみているが,今のところ特に問題なく動いている。
  • JBoss AS 5.1.0.GA のダウンロードページから,jboss-5.1.0.GA-jdk6 をダウンロードしてインストール。
  • postgresql-8.3-605.jdbc4.jar を $JBOSS_HOME/common/lib に入れる。
  • server は default で OK。
  • OpenDolphin の lib に入っている jboss 関連の jar を削除して,代わりに JBoss-5.1.0.GA の $JBOSS_HOME/client に入っている jar を全部と,$JBOSS_HOME/common/lib に入っている hibernate-core.jar を入れる。
  • org.jboss.security.Util.createPasswordHash を使っているメソッドにエラーが出るので,org.jboss.crypto.CryptoUtil.createPasswordHash に書き換える。
  • org.jboss.annotation.ejb.RemoteBinding を使っているメソッドにエラーが出るので,org.jboss.ejb3.annotation.RemoteBinding に書き換える。
  • org.jboss.annotation.security.SecurityDomain を使っているメソッドにエラーが出るので,org.jboss.ejb3.annotation.SecurityDomain に書き換える。
  • persistence.xml のヘッダーの書き換えが必要。
    <persistence xmlns="http://java.sun.com/xml/ns/persistence"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
        version="1.0">
    
  • application.xml と jboss-app.xml は不要。(使いたい場合は,やはりヘッダを書き換えなければならない)
  • ejb/RemoteMainteServiceImpl.java と ejb/RemoteMainteService.java を削除。これは RemotePvtService という JNDI ネームを持っていて,本物の RemotePvtService とぶつかる。
  • delegater/UserDelegater.java に以下のように credential のコードを追加。これを入れないと,せっかく login したのに ejb にアクセスする度に再ログインしようとして失敗しまくる。(4.2.3 では良きに計らってくれていたようだ)

    delegater/UserDelegater.java

    public class UserDelegater extends BusinessDelegater {
        /**
         * ユーザ認証を行う。
         * @return  UserModel
         */
        public UserModel login(DolphinPrincipal principal, String password) throws Exception {
            
            String pk = principal.getFacilityId() + ":" + principal.getUserId();
    //pns^  JBoss AS 5.1.0 対応
            SecurityAssociation.setPrincipal(new SimplePrincipal(pk));
            SecurityAssociation.setCredential(password.toCharArray());
    //pns$
            UsernamePasswordHandler h = new UsernamePasswordHandler(pk, password.toCharArray());
            LoginContext lc = new LoginContext(getSecurityDomain(), h);
            lc.login();
    
            return getUser(pk);
    
  • postgres-ds.xml はそのまま使用可。
  • 構築して ear をデプロイ。

2010年5月16日 (日)

StampPublisher で TaskMonitor を止める

StampPublisher でスタンプ公開した後,公開に成功した後も TaskMonitor が残ってしまうので,消えるようにする。

client/StampPublisher.java

public void publish() {
 ・
 ・
  @Override
  protected void succeeded(Boolean result) {
  logger.debug("Task succeeded");
//pns^  タスクモニタを止める
  this.firePropertyChange("done", null, null);
//pns$
  if (result.booleanValue()) {
 ・
 ・
同様の症状はパスワード変更でもおきるので,client/ChangePassword.java も同様に変更。

2010年5月15日 (土)

サーバを全て ubuntu 8.04LTS に移行

永らくお世話になった debian etch に別れを告げて,サーバを全て ubuntu 8.04LTS に入れ替えた。

  • これまで,xen の dom-0 だけ ubuntu で,dom-U は debian etch で構築していたが,dom-U を全て ubuntu に置き換えた。 dom-0 は 64bit,dom-U は 32bit で構築した。ちなみに,メモリを 4 GB に増設したので,やっと 64bit OS の恩恵にあずかれることになった。dom-U に湯水のようにメモリを割り当てられるのでリッチになった気分。
  • ubuntu orca にしたところ,以下の warning が /var/log/syslog に出るようになったが,動作には問題ないようだ。
    orca console-kit-daemon[3466]: WARNING: Error waiting for native console 1 activation: Invalid argument 
    orca console-kit-daemon[3466]: WARNING: Error waiting for native console 2 activation: Invalid argument
     ・
     ・ 以下 console 62 まで出る。
    
  • JBoss AS も ubuntu でそのまま問題なく走っている。java だから当たり前だが。
  • OpenDolphin のデータ移行は etch で pg_dump で書き出して ubuntu で psql で読み込んだが,postgres のバージョンの違いで,sed で以下の変換が必要になった。移行してしまった後は不要。
    # zcat dolphin_db.dump.gz | sed -e "s/\('.*\\\\\)/E\1/" | psql > /dev/null
    

2010年5月14日 (金)

mac の SelectionListener の挙動不審

スタンプを編集しているとき,テーブルを選択したときに削除ボタンが有効にならないことが時々あった。たまにしか起きないので,原因を探るのが難しかったが,やっと分かった。
SelectionListener で valueChanged が呼ばれるとき,getValueIsAdjusting() が true で呼ばれた後,普通は false でもう一回呼ばれるのだが,なぜかたまに true で呼ばれたまま false が呼ばれないことがあることが分かった。とりあえず,削除ボタンを制御している SelectionListener の getValueAdjusting() 判定をコメントアウトして対応した。クリックをドラッグと認識したまま次のマウスクリックまで放置になっている可能性がある。

 

order/MedicineTablePanel.java

m.addListSelectionListener(new ListSelectionListener() {
  public void valueChanged(ListSelectionEvent e) {
    // 普通に select しても true を1回呼んだ後,知らんプリすることがあるのの workaround
    // true と false で notifySelectedRow() が2回呼ばれてしまうが,この場合問題ない
    // if (e.getValueIsAdjusting() == false) {
      notifySelectedRow();
    //}
  }
});

2010年5月13日 (木)

Mac の JPopupMenu の不具合の回避

Popupmenuok

PopupMenu は,通常左のようにマウスポインタがあるメニューが反転表示される。

Popupmenung

ところが,別のウインドウ(この場合シェーマボックス)にフォーカスがある状態で,フォーカスのないウインドウ(この場合スタンプ箱)を右クリックすると,フォーカスはスタンプ箱にうつり,PopupMenu が表示されるが,この PopupMenu ではメニューの反転表示が出ない。この不具合は macintosh のみでおこり,windows ではおこらない。

この不具合の workaround のため,反転表示を無理矢理出そうと思って MouseListener を組み込んでみたが,一旦この状態に陥ると MouseListener が完全に無視されてしまっていることが分かった。

 いろいろ実験してみたところ,JPopupMenu#show の時点で,マウスボタンが押されている状態だとこの現象がおこることが判明した。そこで,JPopupMenu#show の前に MOUSE_RELEASED イベントを待つようにしたらメニューの反転表示が出るようになった。

ui/MyJPopupMenu.java

package open.dolphin.ui;

import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.util.LinkedList;
import java.util.Queue;
import javax.swing.JPopupMenu;

/**
 * Mac の JPopupMenu の問題を回避
 * @author pns
 */
public class MyJPopupMenu extends JPopupMenu {

  @Override
  public void show(Component invoker, int x, int y) {

    EventQueue systemQueue = Toolkit.getDefaultToolkit().getSystemEventQueue();
    Queue<AWTEvent> queue = new LinkedList<AWTEvent>(); // event をためておく

    // MOUSE_RELEASED event を待つ,その間に発生した event は queue にためておく
    try {
      while(true) {
        AWTEvent evt = systemQueue.getNextEvent();
        queue.offer(evt); // get した event はためておく
        // mouse で popup した場合
        if (evt.getID() == MouseEvent.MOUSE_RELEASED) break;
        // key 入力で popup した場合
        if (evt.getID() == KeyEvent.KEY_RELEASED) break;
      }
    } catch (InterruptedException ex) {
      System.out.println("MyJPopupMenu.java: " + ex);
    }

    // ためておいた event を post する
    while (true) {
      AWTEvent evt = queue.poll();
      if (evt == null) break;
      systemQueue.postEvent(evt);
    }

    super.show(invoker, x, y);
  }
}

Mac OS X 10.6.3 で samba サーバに書き込み不能

以下のメッセージが出て,samba サーバに書き込みできなくなった。
一部の項目へのアクセス権がないため、操作は完了できません。

 

対処法は,smb.conf の [global] で unix extensions = no を設定する。

2010年5月 8日 (土)

LTextStampEditor.java のバグ

stampRole をセットし忘れて,作成したテキストスタンプがカルテにドロップできなくなっていた。orz

order/LTextStampEditor.java

/**
 * 編集したテキストを返す
 * @return ModuleModel
 */
@Override
public Object getValue() {
  ModuleModel model = new ModuleModel();
  TextStampModel stamp = new TextStampModel();
  ModuleInfoBean info = new ModuleInfoBean();

  info.setStampName(titleField.getText().trim());
  info.setEntity(IInfoModel.ENTITY_TEXT);
// ココ↓
  info.setStampRole(IInfoModel.ROLE_TEXT);
  stamp.setText(textPane.getText());

  model.setModel(stamp);
  model.setModuleInfo(info);

  return model;
}

MastarTabPanel を MasterSetPanel で置き換える

order/MasterTabPanel.java と order/MasterSetPanel.java という,よく似たクラスがあって混乱してしまっていた。調べてみたら,MasterTabPanel は MasterSetPanel で完全に置き換えられることが分かったので,MasterTabPanel を使っている部分を全て MasterSetPanel で置き換え,MasterTabPanel は削除した。

2010年5月 5日 (水)

カレンダーの休日表示

増田内科様の util/Holiday.javaを使わせていただいた。

client/LiteCalendarPanel.java

protected class DateRenderer extends DefaultTableCellRenderer {
  private static final long serialVersionUID = 5817292848730765481L;
 ・
 ・
    // 曜日によって ForeColor を変える
    if (col == 0) {
      this.setForeground(getSundayFore());
    } else if (col == 6) {
      this.setForeground(getSaturdayFore());
    } else {
      this.setForeground(getWeekdayFore());
    }
    // 休日 masuda
    SimpleDate sd = tableModel.getDate(row, col);
    if (Holiday.isHoliday(new GregorianCalendar(sd.getYear(), sd.getMonth(), sd.getDay()))) {
      this.setForeground(getSundayFore());
    }
    // masuda

« 2010年4月 | トップページ | 2010年6月 »