JSF2カスタムコンバータでデコードしつつdataTableの背景を変える

今作っているWEBアプリで、画面に一覧を表示して、一覧表示内の項目値によって背景色を変えたいという要件がありました。
で、JSF2のカスタムコンバータを使って、コード値を文言にデコードしつつ、裏でCSSクラスを変えることで背景色が動的に変わる形にしてみました。コンバータは一つで、どのコード体系でデコードするかは、attributeでコンバータに情報を渡す構成になってます、とりあえずは。。

CSS抜粋

.listTable th {
	font-weight: normal;
	color: #ffffff;
	background-color: #339933;
	text-align: center;
	padding: 1px;
	border-right-style: solid;
	border-bottom-style: solid;
	border-right-color: #CCCCCC;
	border-bottom-color: #CCCCCC;
	font-size: 10pt;
	white-space: nowrap;
}

.listTable td {
	padding: 1px;
	background-color: #ffffff;
	border-right-style: solid;
	border-bottom-style: solid;
	border-right-color: #CCCCCC;
	border-bottom-color: #CCCCCC;
	font-size: 11pt;
}

/* 警告表示 */
.cError {
	background-color: #ff0000;
	color: #ffffff;
 	display:block;
}
/* 一部異常表示 */
.cCaution {
	background-color: #ffff00;
 	display:block;
}

/* 対象外表示 */
.cNoTaget {
 	background-color: #a4a4a4;
 	display:block;
}

/* 通常表示 */
.cDefault {
	background-color: #ffffff;
 	display:block;
}

xhtmlのdataTable部分抜粋
※wordsはwords.propertiesのファイル側に定義している項目タイトルの文言です

    <h:dataTable id="Info" var="list" value="#{listView.itemlist}" styleClass="listTable">
      <h:column>
        <f:facet name="header" >#{words.Label_coName}</f:facet>
        <h:outputText value="#{list.coName}"/>
      </h:column>
      <h:column>
        <f:facet name="header" >#{words.Label_coCode}</f:facet>
        <h:outputText value="#{list.coCode}"/>
      </h:column>
      <h:column>
        <f:facet name="header" >#{words.Label_status1}</f:facet>
        <h:outputText value="#{list.status1}">
          <f:converter converterId="statusConverter"/>
          <f:attribute name="ConvertType" value="1"/>
        </h:outputText>
      </h:column>
      <h:column>
        <f:facet name="header" >#{words.Label_status2}</f:facet>
        <h:outputText value="#{list.status2}">
          <f:converter converterId="statusConverter"/>
          <f:attribute name="ConvertType" value="2"/>
        </h:outputText>
      </h:column>
以下省略

    </h:dataTable>

カスタムコンバータ

@FacesConverter(value="statusConverter")
public class StatusConverter implements Converter {

	private static final String DEFVALUE = "X";

	private static final String DEFAULT = "cDefault";
	private static final String NOTARGET = "cNoTaget";
	private static final String CAUTION = "cCaution";
	private static final String ERROR = "cError";
	
	// ConvertType 1 OKNG表示
	private static final Map<String, String> OkNgState;
	static {
		HashMap<String, String> map = new HashMap<String, String>();
		map.put(DEFVALUE, "-");
		map.put("1", "OK");
		map.put("2", "NG");
		OkNgState = Collections.unmodifiableMap(map);
	}
	// ConvertType 2 現在状況表示
	private static final Map<String, String> NowState;
	static {
		HashMap<String, String> map = new HashMap<String, String>();
		map.put(DEFVALUE, "-");
		map.put("0", "処理前");
		map.put("1", "仕掛中");
		map.put("2", "処理済");
		NowState = Collections.unmodifiableMap(map);
	}
	// ConvertType 3 結果表示
	private static final Map<String, String> ResStatus;
	static {
		HashMap<String, String> map = new HashMap<String, String>();
		map.put(DEFVALUE, "-");
		map.put("0", "正常");
		map.put("1", "異常");
		map.put("2", "警告");
		ResStatus = Collections.unmodifiableMap(map);
	}
	//警告表示対象
	private static final Map<String, String> Caution;
	static {
		HashMap<String, String> map = new HashMap<String, String>();
		map.put("異常", ERROR);
		map.put("警告", CAUTION);
		map.put("-", NOTARGET);
		Caution = Collections.unmodifiableMap(map);
	}

	@Override
	public String getAsString(FacesContext arg0, UIComponent arg1, Object arg2) {
		String DispValue="";
		String InnerValue = (String) arg2;
		String StyleClass = DEFAULT;
		
		Map<String,String> ConvertMap = null;
		Map<String,Object> attrMap = arg1.getAttributes();
		
		//f:attribute より変換タイプを取得
		String type = (String)attrMap.get("ConvertType");
		if (! StringUtils.isNumeric(type) ){
			return "";
		}

		switch (Integer.valueOf(type)){
			case 1:
				ConvertMap = OkNgState;
				break;
			case 2:
				ConvertMap = NowState;
				break;
			case 3:
				ConvertMap = ResStatus;
				break;
			default:
				DispValue = InnerValue;
				ConvertMap = null;
				break;
				
		}
		//デコード
		if(ConvertMap != null){
			DispValue = ConvertMap.get(InnerValue);
			//初期値
			if (DispValue == null ){
				DispValue = ConvertMap.get(DEFVALUE);
			}
		}
		//スタイルクラス指定
		if(Caution.get(DispValue) != null){
				StyleClass = Caution.get(DispValue);
			}
		}
		//色変
		if (arg1.getClass() == HtmlOutputText.class){
			HtmlOutputText ui = (HtmlOutputText) arg1;
			ui.setStyleClass(StyleClass);
		}
		return DispValue;
	}
	//こっちは関係無し
	@Override
	public Object getAsObject(FacesContext arg0, UIComponent arg1, String arg2) {
		return null;
	}
}

ManagedBean抜粋
※一覧表示対象のitemInfoの実装は割愛します。プロパティとゲッタ・セッタがあるだけです。。

@ManagedBean(name="listView")
@ViewScoped
public class ListView {
	private List<itemInfo> itemlist;

	public List<itemInfo> getitemlist() {
		//ここでitemlistのデータをセットしてますが省略します
		return itemlist;
	}
以下、省略

こんな感じで表示できそうなので、別途整理して実装します。
上記例では色が変わるのは行でなくセル単位になります。なお、dataTableの場合、その中のoutputTextはdivに置き換えらえるようなので、コンバータではtdのスタイルを変えているのではなくdivが対象になってます。それをCSSでカバーしているのですが、CSSは得意でないので。。

ganymed ssh2を使ってダウンロード画面を作る

今作っているWebアプリケーションでサーバのログファイルをダウンロードしたいという要件があり、そこでganymedを使ってみることにしました。アプリケーションをデプロイしているサーバとは別に複数存在するサーバ上にあるログファイルを画面から選択してクリックでダウンロードという流れです。
 なお、サーバとログファイルが格納されているディレクトリ情報は別途DB等から取る形にして、下記はSFTPでファイルを取得する箇所の抜粋です。

POM.xml ganymed指定

    <dependency>
	  <groupId>ch.ethz.ganymed</groupId>
	  <artifactId>ganymed-ssh2</artifactId>
	  <version>262</version>
     </dependency>

画面に表示するログファイル情報

public class LogFile {
	
	/** ファイル名 */
	private String fileName;
	
	/** 更新日時 */
	private String upDateTime;
	
	/** ディレクトリ */
	private String directory;
	
	/** サイズ */
	private long fileSize;
	
	/** ホスト名 */
	private String hostName;

以下、ゲッタとセッタは割愛

SFTP処理
※サーバは一般的なID・パスワード認証です。

import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.SFTPv3Client;
import ch.ethz.ssh2.SFTPv3DirectoryEntry;
import ch.ethz.ssh2.SFTPv3FileHandle;

/**
 * SFTPファイルアクセスクラス by Ganymed SSH-2
 * */
@Stateless
public class SFTPFileAccess {
	private final int PORT = 22;
	private SFTPv3Client sftp = null;
	private Connection conn = null;
	
	/**
	 * リモートホストへログインします
	 * @param HostName リモートホスト名
	 * @param User ログインユーザー名
	 * @param Password ログインパスワード
	 * @throws IOException 
	 * 
	 * */
	public boolean loginHost(String HostName, String User, String Password) throws IOException{
		conn = new Connection(HostName, PORT);

		conn.connect();
        if (!conn.authenticateWithPassword(User, Password)) {
        	//エラー
        	return false;
        }
        return true;
	}
	
	/**
	 * リモートホスト接続をクローズします
	 * @throws IOException 
	 * 
	 * */
	public void logOutHost() throws IOException{
		if(conn != null){
			conn.close();
		}
        return;
	}

	/**
	 * リモートホスト上の指定ディレクトリのファイル一覧を取得する
	 * @param String RemoteDir ディレクトリ名
	 * @param List<LogFile> セットするファイル一覧
	 * @throws IOException
	 * */
	public void getSFTPList(String RemoteDir, List<LogFile> FileList) throws IOException {
		if(conn == null){
			return;
		}
        sftp = new SFTPv3Client(conn);
        
		List<SFTPv3DirectoryEntry> DirList = sftp.ls(RemoteDir);
		if ( FileList == null ){
			FileList = new ArrayList<LogFile>();
		}
        for (SFTPv3DirectoryEntry Entry : DirList ){
        	//ディレクトリ以外
        	if (!Entry.longEntry.startsWith("d") ){
	        	LogFile file = new LogFile();
	        	file.setDirectory(RemoteDir);
	        	file.setFileName(Entry.filename);
	        	file.setHostName(conn.getHostname());
	        	file.setFileSize(Entry.attributes.size);
                //ファイル更新時間
	        	Date tm = new Date(Entry.attributes.mtime * 1000L);
	        	file.setUpDateTime(DateUtils.format(tm, DateUtils.FORMAT_YYYY_MM_DD_HH_MM_SS));
	        	FileList.add(file);
	    		
        	}
        }
        return;
	}

    /**
     * SFTPファイル受信
     * @param remoteFile 受信元ファイル
     * @param stream 出力ストリーム
     * 
     * */
    public void getFile( String remoteFile, OutputStream stream) throws IOException{
		if(conn == null){
			return;
		}
    	if(sftp == null){
            sftp = new SFTPv3Client(conn);
    	}
        long remoteFileSize = sftp.stat(remoteFile).size;
        // open remote file with read only
        SFTPv3FileHandle sftpFileHandle = sftp.openFileRO(remoteFile);
        
        BufferedOutputStream bos = new BufferedOutputStream(stream);
        		
        byte[] buffer = new byte[1024];
        long offset = 0;
        int readLength = 0;
        while( (readLength = sftp.read(sftpFileHandle, offset, buffer, 0, buffer.length)) != -1){
          bos.write(buffer, 0, readLength);
          offset += readLength;
        }
        // flush & Close
        bos.flush();
        bos.close();
        sftp.closeFile(sftpFileHandle);
        
        // compare
        if (offset == remoteFileSize) {
        } else {
        	System.out.println("failed: file size is different from remote. remote:" + 
              remoteFileSize+", local:"+offset);
        }
        return;
    }
}

画面処理
※HttpServletResponseのOutputStreamに直ファイルを突っ込むので、一時ファイルを置いたりしません。

/**
 * ログダウンロード画面
 * */
@ManagedBean(name="LogDownload")
@SessionScoped
public class LogDownload implements Serializable {
	private static final long serialVersionUID = 1L;

	protected static Logger logger = LogManager.getLogger();
	
    @EJB
    private SFTPFileAccess ftp;
    
    private String selectedServer;

    private List<LogFile> fileList;
    
	public LogDownload() {
		fileList = new ArrayList<LogFile>();
	}

	public List<LogFile> getFileList() {
		return fileList;
	}

	public void setFileList(List<LogFile> fileList) {
		this.fileList = fileList;
	}

	public String getSelectedServer() {
		return this.selectedServer;
	}

	public void setSelectedServer(String selectedServer) {
		this.selectedServer = selectedServer;
	}

	/**
	 * ディレクトリ選択時にファイル一覧を取得
	 * */
	public void getList(){
		try {
			Map<String,String> params = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap();
			String fileDir = params.get("fileDir");

			ftp.loginHost(selectedServer, dao.getServerLoginUser(selectedServer), dao.getServerLoginPasswd(selectedServer));
			
			fileList.clear();
			ftp.getSFTPList(fileDir,fileList);
			
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/**
	 * ファイルダウンロード
	 * */
	public void getFile(){
		Map<String,String> params = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap();
		String fileName = params.get("fileName");
		String fileDir = params.get("fileDir");
	  
		try {
			HttpServletResponse response = (HttpServletResponse) FacesContext.getCurrentInstance() .getExternalContext().getResponse();
			response.setContentType("application/octet-stream");
			response.setHeader("Content-Disposition", "attachment;filename=" + fileName);

			OutputStream a = response.getOutputStream();
			ftp.getFile(fileDir +"/" + fileName , a);
			FacesContext.getCurrentInstance().responseComplete();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

xhtml
※ディレクトリリストの箇所は、上記ソース上割愛しています。
ディレクトリリストの左に置いたボタンクリックで、ファイル一覧を取得し、ファイル一覧のファイル名クリックでダウンロードします。

    <h:form id="listForm">
      <h:dataTable var="dirlist" value="#{LogDownload.directoryList}" rowIndexVar="rowIndex" class="table">
        <h:column>
          <f:facet name="header" >#{words.Label_LogList}</f:facet>
          <h:commandLink value="#{dirlist.INFO_VALUE}" actionListener="#{LogDownload.downLoad}">
            <f:attribute name="directory" value="#{dirlist.INFO_VALUE}"/>
          </h:commandLink>
        </h:column>
        <h:column>
          <h:commandButton id="listget" action="#{LogDownload.getList}" value="#{words.Label_FileListGet}">
            <f:param name="fileDir" value="#{dirlist.INFO_VALUE}" />
            <f:ajax render=":listForm:loglist"/>
          </h:commandButton>
        </h:column>
      </h:dataTable>
    
      <h:dataTable id="loglist" var="list" value="#{LogDownload.fileList}" styleClass="listTable">
        <h:column><f:facet name="header" >ファイル名</f:facet>
        <h:commandLink id="filelistName" value="#{list.fileName}" action="#{LogDownload.getFile}">
        <f:param name="fileName" value="#{list.fileName}" />
        <f:param name="fileDir" value="#{list.directory}" />
        </h:commandLink>
      </h:column>
      <h:column><f:facet name="header">ファイルサイズ</f:facet>
        <h:outputText value="#{list.fileSize}">
          <f:convertNumber groupingUsed="true" />
        </h:outputText>
      </h:column>
      <h:column><f:facet name="header" >ファイル更新日時</f:facet>
        <h:outputText value="#{list.upDateTime}"/>
      </h:column>
    </h:dataTable>    
  </h:form>

エラーハンドリングやサーバの切断、ファイルサイズの数値右寄せ等、細かい箇所がまだ出来ていませんが、とりあえず動きそうです。
最初、ログファイル情報のプロパティ名が先頭大文字とかだと、JSFでエラーになりちょっとハマりました。。

レルムの代わりにJASPICで

 今、企業内で少人数が使用するWEBアプリケーションをWebLogicで動かす前提で作ってますが、ユーザー認証についてどうしようかと要件を確認したところ、少人数使用のアプリなのでLDAP無し、WebLogicに登録・管理したくないのでWeblogicのレルムも使わないでとのこと。となると従来のDBにユーザーテーブル作ってユーザー管理画面とかも作る感じになります。
 ユーザーには数パターンあるうちの1つの権限を付与すれば使える画面と使えない画面が決まるので、基本的にURLで制御できる範囲です。で、いろいろ調べましたが、下記を参考にして、JASPIC(Java Authentication Service Provider Interface for Containers)というJavaEE7仕様に含まれているものを使ってみる事にしました。
http://arjan-tijms.omnifaces.org/2012/11/implementing-container-authentication.html
ここのSETP5にあるServerAuthModule実装のvalidateRequestに認証処理を書き込む形のようです。
下記のサンプルソースも参考になりました。
https://github.com/erik-wramner/YubikeyAuth/tree/master/yubi-jaspic-example

但し、WebLogicで動かす場合は、web.xmlにだけ設定すればよいだけでなく、weblogic.xmlにも同じような設定が必要みたいです。

web.xml

  <security-role>
    <description>管理者</description>
    <role-name>Managers</role-name>
  </security-role>
  <security-role>
    <description>利用者</description>
    <role-name>Users</role-name>
  </security-role>
  
  <security-constraint>
    <web-resource-collection>
      <web-resource-name>manager page</web-resource-name>
      <url-pattern>/manage/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <role-name>Managers</role-name>
    </auth-constraint>
  </security-constraint>

  <security-constraint>
    <web-resource-collection>
      <web-resource-name>user page</web-resource-name>
      <url-pattern>/user/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <role-name>Users</role-name>
    </auth-constraint>
  </security-constraint>

weblogic.xml

    <wls:security-role-assignment>
      <wls:role-name>Managers</wls:role-name>
      <wls:principal-name>Managers</wls:principal-name>
    </wls:security-role-assignment>
    
    <wls:security-role-assignment>
      <wls:role-name>Users</wls:role-name>
      <wls:principal-name>Users</wls:principal-name>
    </wls:security-role-assignment>

デプロイするとそのインスタンス全体に影響し、開発環境では管理サーバで動かしているので管理コンソールに入れなくなります。。

カテゴリー: Java

TeraTermマクロでプロセスをKILL

UNIXサーバ作業の自動化として、特定のプロセスをKILLするマクロを書きました。
前提として
・停止対象のプロセスはJavaの常駐JOBが3つ
・プロセス停止前にチェック処理が必要
このチェック処理が複雑なので、マクロにしました。
下記は、サーバにログオンされた状態以降の抜粋です。チェック処理と最後のログオフは省略してます。
実際のツールはHTAで作業メニュー画面があり、画面操作でマクロを起動する構成にしました。

strdim OUTPUTBUF 500

; KILLする対象のプロセスがPSコマンドで表示される名前
strdim CHK_PROCESS_NAMES 3
CHK_PROCESS_NAMES[0] = 'jp.aaa.bbb.jobA'
CHK_PROCESS_NAMES[1] = 'jp.aaa.bbb.jobB'
CHK_PROCESS_NAMES[2] = 'jp.aaa.bbb.jobC'

; プロセスIDをゲットできたかのフラグ
CHK_PROCESS_RES = 0

timeout = 5

for iii 0 2
   
   CHK_PROCESS_NAME = CHK_PROCESS_NAMES[iii]
   ;; プロセスID取得subコール
   call GET_PROCESSID
   if CHK_PROCESS_RES <> 1 then
      messagebox '停止対象のプロセスが起動していません。'#13'状態を確認してくだい(OKで次プロセスの停止に進みます)' CHK_PROCESS_NAME
   else
      strlen CHK_PROCESS_ID
      if result = 0 then
         messagebox '停止対象のプロセスIDが取得出来ませんでした。'#13'状態を確認してくだい(OKで続行します)' CHK_PROCESS_NAME
      else
         ;; KILL前のチェック処理(省略・・)

         ;; プロセスKILL
         CMD = 'kill -9 '
         strconcat CMD CHK_PROCESS_ID
         sendln CMD
         wait '#' '$'

         ;; プロセスID取得subコール(Killしたのに残っていたら警告)
         call GET_PROCESSID
         if CHK_PROCESS_RES <> 2 then
            messagebox 'プロセスが停止できていません。'#13'状態を確認してくだい(OKで続行します)' CHK_PROCESS_NAME
         endif
      endif
   endif
next

;;マクロ終了
end

;;; sub プロセス状態確認 start
:GET_PROCESSID
CHK_PROCESS_RES = 2
OUTPUTCNT = 0
flushrecv

setsync 1
CMD = 'ps -ef | grep '
strconcat CMD CHK_PROCESS_NAME
sendln CMD

recvln
while result = 1
   recvln
   if result = 0 then
      break
   endif
   OUTPUTBUF[OUTPUTCNT] = inputstr
   OUTPUTCNT = OUTPUTCNT + 1
endwhile
setsync 0

if OUTPUTCNT > 0 then
   ;; Javaプロセスか確認(Grepでない)
   CHKSTR = '/bin/java'
   for i 0 OUTPUTCNT - 1
      strscan OUTPUTBUF[i] CHKSTR
      if result > 0 then
         ;; プロセスIDセット
         strcopy OUTPUTBUF[i] 10 8 CHK_PROCESS_ID
         CHK_PROCESS_RES = 1
         break
      endif
   next
endif

return
;;; sub プロセス状態確認 end

FilterでWeb要求・応答をログ出力してみる

今、リクエストを受信した後に対外接続を行うシステムを作ってますが、がつがつと外部に繋ぐのも気が引けるという事で、、そのシミュレータとして簡単なWebAPも作りました。
APサーバは、WebLogic11G。作っているAPの仕様上、POSTをサーブレット処理してます。
で、結局Oracleのページに乗っていたグッドなサンプルをそのままパクってみました(汗)
Oracle Containers for J2EE サーブレット開発者ガイド
Request処理は解るのですが、Responseの方はFilter内でラップするというちょっと自分では考え付かない内容でした。。
上記に載っている、FilterServletOutputStreamとGenericResponseWrapperは、深く考えずに同一パッケージ内にそのままクラスとして作ります。
で、Filterクラスを作ってログを出力します。よく使っているlog4j2でやってます。

package jp.esoro.http;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class PostFilter implements Filter {
	protected static Logger logger = LogManager.getLogger();
	
	@Override
	public void destroy() {
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
 		try {
 			//要求
			HttpServletRequest req = (HttpServletRequest) request;
			
			BufferedReader body = new BufferedReader(req.getReader());
			String ret = body.readLine();
			//要求ロギング
			logger.info(req.getRemoteAddr()+"->"+req.getLocalAddr()+":"+req.getLocalPort()+req.getContextPath() + ":" + req.getMethod() + " RCV:"+ret);

			OutputStream out = response.getOutputStream();
			
			GenericResponseWrapper wrapper = 
			         new GenericResponseWrapper((HttpServletResponse) response); 
			//この後、サーブレット側処理に移ります
			chain.doFilter(request, wrapper);	
			
			//ラップしたwrapperからサーブレットの処理結果を取得
			byte[] data = wrapper.getData();
 			//OutputStreamに書き込んで応答
			out.write(data);
			out.close();
			//Shift-JISでString変換
			String result = new String(data, "Shift-JIS");
			//応答ロギング
			logger.info(req.getRemoteAddr()+"<-"+req.getLocalAddr()+ ":"+req.getLocalPort()+ req.getContextPath() + ":" + req.getMethod() + " SND:"+result);

		} catch (IOException e) {
			logger.error(e.getMessage());
		}
	}
	@Override
	public void init(FilterConfig arg0) throws ServletException {
	}
}

で、web.xmlに上記Filterをセットします

 	<!-- Filter -->
	<filter>
		<filter-name>PostFilter</filter-name>
		<filter-class>jp.esoro.http.PostFilter</filter-class>
	</filter>
	<filter-mapping>
        <filter-name>PostFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

最初サーブレットはアノテーション@WebServletで書きましたが、どうも11Gだとダメっぽいのでweb.xmlにservletをMappingする事にしました。シミュレータは本当に簡単なAPなので。。
開発環境は他のプロジェクトで使うWebLogic12cのままで11Gを別途用意しなかったんですが、実行環境が違うとデプロイに失敗して結構メンドクサイです。コンパイラーJavaのバージョンをちゃんと確認してなかったからなんですけど、、

TeraTermマクロのfileopenで失敗

去年作ったTeraTermマクロのサーバ作業の自動化ツールで1年以上問題無かった箇所でエラーになるとの事で見たら、端末のセキュリティレベルが変わっていてマクロのfileopenで失敗している事が判明。fileopenしているファイルが書き込めないように端末設定が変わっていたのが原因で、マクロのfileopenでは書き込めないとエラーになるんですね。
 で、マクロの仕様をネットで見たらfileopenの仕様に読み取り専用モードのオプションが4.85以降で追加されてるじゃないですか!
 かといって、ツールで使っているTeraTermマクロをバージョンアップするのも、20パターンくらいの作業を自動化しているのでちょっと躊躇。。
 作業自動化ツールをマクロで作ることにしたのは、基本ミドル・運用アプリ系の導入無し、複数サーバで同じような作業があるし、サーバ側に作業自動化シェルとか置くと、ここのサーバ運用的に変更の敷居が高いという環境上の要件からなんですが、マクロにしたらしたらで端末の設定変更でなくマクロを変えればいいじゃん的な発想に変わってしまうという、、まさにジレンマですね。変えやすいものは変われるけれど、それを維持するのは結構メンドクサイんです。。このメンドクサイという事実がシステムの開発・運用コストに直結しているハズなんですけどね。。

WLSのデータソース設定せずにJPA

今筆者が作っているWEBアプリケーションのDB周り実装です。
インフラ的には、主にWLS12+OracleDBとなっていますが、社内特定部署に利用が限定されたアプリケーションなので、DBセッションは「節約したい」という微妙な要件です。さらに、WLSデータソース設定はしたくないとの希望です。
せっかくEE使うんで、JPA前提でやる事にしますが、WEBに出てる殆どのネタがWLSのデータソースを使っているんですよね。
セッションプールは別途OracleUCPの独自実装があるので、persistance.xmlで指定せずに、UCPのデータソースをプログラム上からエンティティマネージャに指定するというイメージなんですが、WEBで調べながら実際に動くまで結構時間掛かりました。。

まず、EclipseのプロジェクトファセットにJPAを追加するとpersistence.xmlが出来ますが、データソースをここでは設定しないのでエンティティクラス定義のみ書きます。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" 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_2_0.xsd">
	<persistence-unit name="DBresource" transaction-type="RESOURCE_LOCAL">
		<class>jp.xxx.zzz.model.user.LoginUser</class>
他クラス定義省略
   	<exclude-unlisted-classes>true</exclude-unlisted-classes>
	</persistence-unit> 
</persistence>

次にエンティティクラス(テーブル定義みたいなもの)

@Table(name="USER_MST")
@Entity
public class LoginUser implements Serializable{
	
	private static final long serialVersionUID = 1L;
	
	/** ユーザーID*/ @Id @NotNull @Size(max=20,min=7) 
	private String USER_ID;
	/** ユーザーパスワード*/
	private String USER_PASS;
	/** ユーザー種別*/ @NotNull
	private String USER_TYPE;
以下、項目とゲッタセッタは省略

その次が、データアクセスインターフェース(データ操作定義)

/**
 * ログインユーザーマスタ 
 * Data access object interface
 * */
public interface LoginUserDao {

    /***
     * データ1件取得
     * @return キー
     * */
    public LoginUser find(Object id);
    
    /***
     * データ新規作成
     * */
	public void create(LoginUser entitiy);

    /**
     * データ更新
     * */
    public void update(LoginUser entitiy);

    /**
     * データ全件取得
     * */
    public List<LoginUser> getAll();
    
    /**
     * データ件数取得
     * */
    public long  getAllCnt();
}

さらに上記インターフェースをJPA実装したデータアクセスオブジェクト(DAO)

/**
 * ログインユーザーマスタ
 * Data access object (JPA)
 * */
@Stateless
//@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class LoginUserDaoJpa implements LoginUserDao{
	protected static Logger logger = LogManager.getLogger();
	public EntityManager  em = null;

	/**
	 * コンストラクタ
	 * */
	public LoginUserDaoJpa() {
		super();
		em = DBEntityManager.getEntityManager();
	}
	/**
	 * データ検索
	 * @param id (key)
	 * @return LoginUser
	 * */
	@Override
	public LoginUser find(Object id) {
		return em.find(LoginUser.class, id);
	}
	/**
	 * データ新規登録
	 * @param ServerConf 
	 * */
	@Override
	public void create(LoginUser entity) {
 		EntityTransaction txn = em.getTransaction();
 		txn.begin();

    	if ( ! em.getTransaction().isActive() ){
    		em.getTransaction().begin();
    	}
		em.persist(entity);
        txn.commit();
	}

	/**
	 * ユーザーマスタの全取得
	 * @return ログインユーザーマスタのリスト
	 * */
	@SuppressWarnings("unchecked")
	@Override
	public  List<LoginUser>  getAll() {
		//後日追加
		em.clear();
		List<LoginUser> result = em.createQuery("SELECT c FROM LoginUser c WHERE c.STATUS_FLG < :SFLG ORDER BY c.USER_ID")
					.setParameter("SFLG", "9")
					.getResultList();
    	return result;
    } 	
	/**
	 * ユーザーマスタより業務メンバーの取得
	 * @return ログインユーザーマスタのリスト
	 * */
	@SuppressWarnings("unchecked")
	public  List<LoginUser>  getBizMember() {
		//後日追加
		em.clear();
		List<LoginUser> result = em.createQuery("SELECT c FROM LoginUser c WHERE c.USER_TYPE < '5' AND c.STATUS_FLG < :SFLG ORDER BY c.USER_ID")
					.setParameter("SFLG", "9")
					.getResultList();
    	return result;
    }
    /**
     * データ更新
     * @param ServerConf
     * */
	@Override
 	public void update(LoginUser entity) {
 		EntityTransaction txn = em.getTransaction();
 		txn.begin();

    	if ( ! em.getTransaction().isActive() ){
    		em.getTransaction().begin();
    	}
        em.merge(entity);
        txn.commit();
    }
 	/**
 	 * 件数取得
 	 * */
	public long  getAllCnt() {
    	return em.createQuery("SELECT c FROM UserMaster c").getMaxResults();
    }
}

上記DAOのコンストラクタで読んでいるEntityManagerを取得するシングルトンクラス

public class DBEntityManager {
	protected static Logger logger = LogManager.getLogger();
	private static Map<String, Object> configOverrides;
	
	static {
		Map<String, Object> map = new HashMap<String, Object>();		
		try {
			//ここのDBConnectionMgrとは、UCPを使用してデータソースを戻す独自実装です・・
			//独自実装内で各種JDBC設定がされています・・
			map.put("javax.persistence.nonJtaDataSource", DBConnectionMgr.getDataSource());
		} catch (SQLException e) {
			logger.error("DBConnectionPool JpaEntityManager put Error!", e);
		}
		configOverrides = Collections.unmodifiableMap(map);
	}
	/**
	 * EntityManagerを返します
	 * */
	public static EntityManager getEntityManager(){
		return Persistence.createEntityManagerFactory("DBresource", configOverrides).createEntityManager();
	}
}

上記を呼び出しているJava画面ソース

/**
 * ユーザーマスタ管理画面処理(ユーザー作成・更新・削除・パスワード変更共通)
 * 
 * */
@ManagedBean(name="UserManager")
@ViewScoped
public class UserManager implements Serializable {
	private static final long serialVersionUID = 1L;

    @EJB
    private LoginUserDao dao;
	
	List<LoginUser> List;
	
	@ManagedProperty("#{userDto}")
	LoginUserDto user;

	public LoginUserDto getUser() {
		return user;
	}

	public void setUser(LoginUserDto user) {
		this.user = user;
	}

	/** 以下画面項目 */
	public List<LoginUser> getList() {
    	List = dao.getAll();
		return List;
	}

	public void setList(List<LoginUser> list) {
		List = list;
	}
以下省略

最後に上記呼び出し箇所のXHMLの抜粋

  <h:dataTable var="item" value="#{UserManager.list}" columnClasses="userid,username" >
    <h:column>
      <f:facet name="header">#{words.Label_UserID}</f:facet>
      <h:outputText value="#{item.USER_ID}" />
    </h:column>
  </h:dataTable>

後で、テストしたところ、DB更新は出来ていてもクエリーでデータを取得すると反映していませんでした。永続化コンテキストからみの事象と思われますが、細かい仕様を調査するのは骨が折れるので、、、クエリー取得前にEntityManagerをclearする事で対処としました。

VBSでFTP

あるWindowsサーバのログファイルを別サーバへ定期的にFTP送信したいとの事で、VBSで簡単なスクリプトを作りました。これまでは、バッチやスクリプトでFTP用のコマンドファイルを作り、FTP -S:ファイル名で実行するような感じだったのですが、最近は、スクリプトだけでできるようです。
下記例は、D:\log\tempというディレクトリに一つのzipファイルが置かれており、そのファイルを送信するという前提です。
ちなみにzipファイル自体の名前でstrSrcを指定するとCopyHereが勝手にZipの中身を解凍した状態で送信されます。。そういや、CopyHereでzipの圧縮や解凍もできるからそういう事なんでしょうか。。恐るべしCopyHere

'FTP送信指定
Const HostName = "192.168.1.101"
Const UserID = "userid"
Const UserPass = "password"
Const DestDir ="Upload"

Dim strDest,strSrc, UpFileName, UpFileSize, oSH, objDest

strSrc="D:\log\temp"

strDest = "ftp://" & UserID & ":" & UserPass & "@" & HostName & "/" & DestDir

Set oSH = CreateObject("Shell.Application")
Set objDest = oSH.NameSpace(strDest)

WScript.Echo "[" & strSrc & "]FTP転送対象"

For Each objItem In oSH.NameSpace(strSrc).Items
WScript.Echo "[" & strSrc & "]FTP転送ファイル [" & objItem.Name & "]," & objItem.Size
	UpFileName = objItem.Name
	UpFileSize = objItem.Size
    '&H04 + &H10は効かない・・・
	objDest.CopyHere objItem, &H04 + &H10
Next
'非同期なのでスリープで待つ・・・
Wscript.Sleep 15000

'リモートディレクトリの送信ファイル存在確認
Dim CheckOK, iWaitCnt
CheckOK = False
For iWaitCnt = 0 To 10
Set objDest = oSH.NameSpace(strDest)
    For Each objItems In objDest.Items
		If UpFileName = objItems.Name Then
    		'リモートファイルのサイズは取れないみたい・・・
       		WScript.Echo "[" & strSrc & "]FTP転送ファイル確認 [" & objItems.Name & "]," & objItems.Size
	   		CheckOK = True
	   		Exit For
		End If
    Next
	If CheckOK = True Then
		Exit For
	End If
    Wscript.Sleep 10000
    WScript.Echo "FTP転送待ち[" & iWaitCnt & "]" 
Next
If CheckOK = False Then
	WScript.Echo "FTP転送に失敗しました " & Err.Number
Else
	WScript.Echo "FTP転送正常終了"
End If

でも、このCopyHere、FTPでは非同期のようで送信終了タイミングが解らなくスリープ待ちせざるを得なかったり、上書き確認のダイアログがどうしても消せない等、ちょっとクセが強いです。。
今回の要件的にはまあ上書きの対処は見切りました。確実性が必要な処理だとこのやり方はしないかな~

JAX-RSを使ってみる

今筆者が作っているサブシステムは、複数種類の定期ジョブとWeblogicで動作させる画面アプリと組合せなんですが、定期ジョブの実装方式として、JAX-RSを検討しました。

なんでジョブをJAX-RSで作るのか?という所なんですが、Weblogicデプロイモジュールに含めることでジョブ実行時のメモリ消費をインフラ設計範囲内に収める為(Coherenceキャッシュへのアクセスがある)と、ジョブ変更時のモジュール管理をサブシステムとして統一したいという理由です。

あと、ジョブ起動については既存のジョブ管理ツールに統一したいとの事なので、EJBスケジューリングは使えない前提です。というか、EJBスケジューリングの挙動検証がしきれてないので、お勧めするのを躊躇したという所なんですが。。

当然、そのままだと外部からアクセスできるので、セキュリティ対策としては要求元IPでフィルタする形で対応する事としてます。

簡単な汎用POSTリクエストモジュールを作成した後、JAX-RSとして動作する処理をJavaEEアプリ内に作成します。

Weblogicデフォルトではなぜか動かなかったので、
weblogic.jaxrs.server.portable.servlet.ServletContainerを使わず、
POMにjerseyを追加

<dependency>
  <groupId>com.sun.jersey</groupId>
  <artifactId>jersey-core</artifactId>
  <version>1.19</version>
</dependency>

web.xmlを上記に合わせる

  <servlet>
    <description>JAX-RS Tools Generated - Do not modify</description>
    <servlet-name>JAX-RS Servlet</servlet-name>
      <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> 
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>JAX-RS Servlet</servlet-name>
    <url-pattern>/batch/*</url-pattern>
  </servlet-mapping>

JAX-RSソース(コンテキストルート/batch/JOB1のURLでPOST実行)

/**
 * バッチ処理
 * */
@Path("/JOB1")
public class BatchExecute{
    public String hello(@Context HttpServletRequest request) throws InterruptedException {
        return "Batch GET Hello!";
    }
    /**
     * ジョブ実行
     * */
    @POST
    public Response execute(@Context HttpServletRequest request) {
       //POSTパラメータ取得
       String Interval = request.getParameter("PARAM1");
      ・・・以下省略
    }
}

ジョブ管理の側はログファイルの検知をトリガーにするので、どのプロセスがログに書いたかはどうでもよく、これで行こうと思います

追伸 1年経って下記のような事もありました。。
weblogic.xmlのprefer-web-inf-classesをtrueにしたらハマった・・

リファクタリング中にパニくる。。

現在、複数のアプリケーションを一人で同時作成しているのですが、ソースがぐちゃぐちゃになってきたので、今更ながら共通処理を別プロジェクトにまとめて一元管理する事にしました。
WebApのプロジェクトでは、ビルドパスの追加と、プロジェクトのプロパティ→Webデプロイメント・アッセンブリーで、共通処理プロジェクトを追加すればサーバ実行時に動くようになるようです。
 その後、ソースを共通処理プロジェクト側に移動させていたのですが、動作確認するとDB接続箇所で意味不明のエラーが発生し、接続できなくなってしまいました。

<2015/06/24 13時14分30秒 JST> <[ServletContext@25608288[app:OfficeAp module:officeap path:null spec-version:3.0]] Root cause of ServletException. javax.security.auth.message.AuthException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: java.sql.SQLException: ユニバーサル接続プールを起動できません: oracle.ucp.UniversalConnectionPoolException: データソースから接続を取得できません: java.sql.SQLException: 接続プロパティ: 書式エラー: Property is 'v$session.program' and value is 'XX.XXXX.XXX.XXXXXXXXXX.common.DBConnectionManager'

他にもいろいろソースを移動させていたので、何が原因か解らなくちょっとしたパニック状態。。
数時間試行錯誤した後、エラーメッセージをちゃんと見直してみる。
v$sessionのprogramにクラス名がセットできないみたいだけど。。なぜ?

結果、パッケージ名が長くなってセット出来なくなっただけでした。。
48文字までしかダメみたい。

カテゴリー: Java