end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

javaのnative2ascii なfile をpython で読むには、byte.decode('unicode-escape')

つまり、以下のような python script になります。

#!python
# -*- coding: utf-8 -*-

import copy
import pprint
import re

src_encode = 'utf-8'

class Conv4Properties():
    def __init__(self):
        pass

    def load_native2ascii(self,file_path):
        ret_lines = []
        regexp = re.compile(r'^([^=]+)=(.+)$')
        
        with open(file_path, 'r') as fh:
            for line in fh:
                line_byte = line.strip().encode()
                line_str = line_byte.decode('unicode-escape')

                result_re = regexp.search(line_str)
                if result_re:
                    ret_lines.append([result_re.group(1),result_re.group(2)])
                else:
                    ret_lines.append([line_str])
                    
        self.lines = ret_lines
        return ret_lines

    def set_atri_key_val(self,atri_key,atri_val):
        
        for line in (self.lines):
            if line[0] == atri_key:
                line[1] = atri_val
                return self.lines

        self.lines.append([atri_key,atri_val])
        return self.lines

SpringBoot for java における app root (path)の routing

メモ。

以下のように書くと、 /hello/world な 2段? endpoint でroutingできます。

package com.coltware.spring.sample.api;

import com.coltware.spring.sample.model.WorldModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/hello")
public class HelloController {

    private static final Logger log
    = LoggerFactory.getLogger(HelloController.class);

    @Autowired
    WorldModel model;

    @GetMapping(value = "/world")
    public WorldModel world(){
        return model;
    }

}

SpringBoot for java における @Configuration

メモ。

以下のように書くと、application.properties にある設定を @Configuration 経由で参照できます。

src/main/resources/application.properties

sample.world = Hello sample

src/main/java/jp/end0tknr/config/SampleConfiguration.java

package jp.end0tknr.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import jp.end0tknr.model.WorldModel;

@Configuration
public class SampleConfiguration {

    @Value("${sample.world:World}") // "World"はdefault値
    String world;

    @Bean
    public WorldModel worldModel(){
        WorldModel model =  new WorldModel();
        model.setValue(world);
        return model;
    }
}

src/main/java/jp/end0tknr/model/WorldModel.java

package jp.end0tknr.model;

public class WorldModel {

    String value;

    public String getValue(){
        return value;
    }

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

phpunit による php の 自動単体テスト( unittet )練習

タイトルの通りです。 phpunit による自動単体テストは触ったことがありませんでしたので。

phpunit の install

と言っても、phpunit.phar をダウンロードするだけです。

$ cd /home/end0tknr/prj01/hon/cgi-bin2/test
$ wget https://phar.phpunit.de/phpunit-8.5.9.phar
$ chmod 755 phpunit-8.5.9.phar
$ phpunit-8.5.9.phar --version
PHPUnit 8.5.9 by Sebastian Bergmann and contributors.

xdebug の install

今回は、code coverage を計測しますので、xdebug も installします。

$ sudo yum install php72-php-xdebug

## 以下の2行を追加。
$ sudo vi /etc/php.ini
zend_extension=/opt/remi/php72/root/usr/lib64/php/modules/xdebug.so
xdebug.mode=coverage

testファイルの準備 - TestHelmForm4Sendmail.php

以下の通り

<?php
use PHPUnit\Framework\TestCase;

require_once 'lib/HelmForm4Sendmail.php';

// ※ TEST対象CLASSにある public methodに対してテストします
// ※ assert~() の fail 時にテストを停止したくないが
//    実現方法不明。python のsubTest()のようなものが欲しい

class StackTest extends TestCase {
    protected $obj;
    protected $assign_forms = [];

    protected function setUp(): void {
        // refer to https://www.php.net/manual/ja/function.assert-options.php
        ini_set("assert.warning",1);
        ini_set("assert.bail",   0);
        
        $this->obj = new HelmForm4Sendmail();

        $this->assign_forms = $this->obj->assign_forms_def();
    }

    private function rand_assign_form_def() {
        $i = rand(0,count($this->assign_forms) -1);
        return $this->assign_forms[$i];
    }

    public function test_validate_mailtos_str() {
        $func_name = __CLASS__." ".__FUNCTION__;
        $test_cases = [
            ["test1@example.com,test2@example.com",
             ["test1@example.com","test2@example.com"]],
            ["test1@example.com",      ["test1@example.com"]],
            ["test1,test2@example.com", ["test2@example.com"]]];
        
        foreach ($test_cases as $test_case ) {
            $result_valid = $this->obj->validate_mailtos_str($test_case[0]);
            
            $tmp_msg = $func_name." ".$test_case[0];
            $this->assertEquals($result_valid,$test_case[1],$tmp_msg);
        }

        

    }
    
    public function test_is_valid_address() {
        $func_name = __CLASS__." ".__FUNCTION__;
        $test_cases = [["test",            false],
                       ["test@example.com",true ]];
        foreach ($test_cases as $test_case ) {
            $result_valid = $this->obj->is_valid_address($test_case[0]);
            
            $tmp_msg = $func_name." ".$test_case[0] ." ".$test_case[1];
            $this->assertEquals($result_valid,$test_case[1],$tmp_msg);
        }
    }

    
    public function test_mail_info_defs() {
        $func_name = __CLASS__." ".__FUNCTION__;

        $mail_rule_keys = [
            "rule_name","formid","subject",
            "fromname","fromaddress","target_to","system_to"];
        $mail_rules = $this->obj->mail_info_defs();

        foreach ($mail_rules as $mail_rule ) {
            foreach ($mail_rule_keys as $mail_rule_key ) {
                $rule_val = $mail_rule[$mail_rule_key];
                $tmp_msg = "$func_name $mail_rule_name $mail_rule_key = '$rule_val'";
                $this->assertGreaterThanOrEqual(0, count($rule_val), $tmp_msg);
            }
        }
    }

    
    public function test_mail_info_def() {
        $func_name = __CLASS__." ".__FUNCTION__;

        $mail_rule_keys = [
            "rule_name","formid","subject",
            "fromname","fromaddress","target_to","system_to"];

        foreach ($this->assign_forms as $assign_form_def ) {
            $mail_rule_name = $assign_form_def["notify_mail"];
            $mail_rule = $this->obj->mail_info_def($mail_rule_name);
            
            foreach ($mail_rule_keys as $mail_rule_key ) {
                $rule_val = $mail_rule[$mail_rule_key];
                $tmp_msg = "$func_name $mail_rule_name $mail_rule_key = '$rule_val'";
                $this->assertGreaterThanOrEqual(0, count($rule_val), $tmp_msg);
            }
        }
    }
}

phpunit の実行

以上で、準備完了ですので、以下のように実行 & 表示されます。

$ pwd
/home/end0tknr/prj01/hon/cgi-bin2

$ ./test/phpunit-8.5.9.phar --verbose \
     --coverage-text \
     --whitelist=/home/end0tknr/prj01/hon/cgi-bin2/lib \
     ./test/TestHelmForm4Sendmail.php
PHPUnit 8.5.9 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.2.24 with Xdebug 3.0.3

....                                        4 / 4 (100%)
Time: 461 ms, Memory: 16.00 MB
OK (4 tests, 537 assertions)

Code Coverage Report:      
  2021-04-08 15:24:02      
 Summary:                  
  Classes:  0.00% (0/8)    
  Methods:  9.72% (7/72)   
  Lines:    8.74% (93/1064)

HelmForm
  Methods:  29.41% ( 5/17)   Lines:  35.47% ( 61/172)
HelmForm4Sendmail
  Methods:  22.22% ( 2/ 9)   Lines:  28.83% ( 32/111)

再x2 - antlr4-python3-runtime for python3 による java source の parse / 構文解析 / コメント抽出

再 - antlr4-python3-runtime for python3 による java source の parse / 構文解析 - end0tknr's kipple - web写経開発

先程、記載した上記entry を再度、修正。

単純に parse / 構文解析を行うと、コメント分が削除される為、 ast.ast_processor.py 内で、無理やり?、COMMENT & LINE_COMMNET を収集。

詳細は、ast.ast_processor.py をご覧下さい。

▲ 1,000行を超えるsrcの場合、parseできない場合があるようです。

ast_analyze_executor.py

# -*- coding: utf-8 -*-

import os
import sys
sys.path.append( os.path.dirname(__file__) )

import glob
import logging.config
from ast.ast_processor import AstProcessor
from ast.basic_info_listener import BasicInfoListener
import re
import sys
import yaml
import pprint

log_conf = './log_conf.yaml'  # log設定は自分でも怪しいと思う
line_feed_str = "\r\n"

def main():
    logging.config.dictConfig(yaml.load(open(log_conf).read(),
                                        Loader=yaml.SafeLoader))
    logger = logging.getLogger('mainLogger')

    java_base_dir = sys.argv[1]

    if os.path.isdir(java_base_dir):
        java_paths = glob.glob(os.path.join(java_base_dir,'**/*.java'),
                               recursive=True)
    else:
        java_paths = [java_base_dir]

    src_infos = {}
    for java_path in sorted(java_paths):
        print( java_path )

        try:
            src_info_and_comments = \
                AstProcessor(logging, BasicInfoListener()).execute(java_path)
        except:
            print("ERROR fail AstProcessor.execute()", java_path)
            continue

        src_info = src_info_and_comments[0]
        comments = src_info_and_comments[1]

        # 上下に連続するcomment は merge
        comments = merge_comments(comments,src_info)
        # 近接するsrcにcommentを添付?
        attach_comments_to_src(comments,src_info)

        src_infos[java_path] = src_info

    for java_path in src_infos:
        src_info = src_infos[java_path]
        print(pprint.pformat(src_info, width=80))

def attach_comments_to_src(comments,src_info):
    for comment in comments:
        for offset in [1,2]:
            start_line = comment["pos"]["start_line"]
            stop_line  = comment["pos"]["stop_line" ] + offset
        
            found_src = find_src_by_line_no_range([start_line,stop_line],
                                                  src_info)
            if not found_src:
                continue
            #以下の場合 body_srcに既にコメントが含まれている為、pass
            if found_src["pos"]["start_line"] < start_line and \
               stop_line < found_src["pos"]["stop_line"]:
                continue
            found_src["comment"] = comment
            break
        
def merge_comments(comments,src_info):

    ret_comments = []
    org_comments_size = len(comments)
    i = 0
    while i+1 < org_comments_size:
        comment_0 = comments[i]
        comment_1 = comments[i+1]
        merge_result = merge_comments_sub(comment_0,comment_1,src_info)
        if len(merge_result) == 2:
            ret_comments.append(merge_result[0])
        i += 1
    return ret_comments
        
def merge_comments_sub(comment_0,comment_1,src_info):
    if comment_0["pos"]["stop_line"] +1 != comment_1["pos"]["start_line"]:
        return [comment_0,comment_1]

    line_nos = [comment_0["pos"]["start_line"],
                comment_1["pos"]["stop_line"]]

    # comment 範囲に、実際のsrcがある場合、merge対象外
    found_src = find_src_by_line_no_range(line_nos,src_info)
    if found_src:
        return [comment_0,comment_1]

    comment_1["text"] = comment_0["text"] + line_feed_str +comment_1["text"]
    comment_1["pos"]["start_line"]   = comment_0["pos"]["start_line"]
    comment_1["pos"]["start_column"] = comment_0["pos"]["start_column"]
    return [comment_1]

    
def find_src_by_line_no_range(line_nos,src_info):

    if type(src_info) is dict:
        for atri_key in src_info:
            atri_val = src_info[atri_key]
            if atri_key == "pos":
                if(src_info[atri_key]["start_line"] <= line_nos[0] and \
                   line_nos[0] <= src_info[atri_key]["stop_line"] ):
                    return src_info
                elif(src_info[atri_key]["start_line"] <= line_nos[1] and \
                     line_nos[1] <= src_info[atri_key]["stop_line"]):
                    return src_info
            
            if type(atri_val) is list or type(atri_val) is dict:
                found_src = find_src_by_line_no_range(line_nos,atri_val)
                if found_src:
                    return found_src
                
    elif type(src_info) is list:
        for atri_val in src_info:
            
            if type(atri_val) is list or type(atri_val) is dict:
                found_src = find_src_by_line_no_range(line_nos,atri_val)
                if found_src:
                    return found_src
    return None


if __name__ == "__main__":
    main()

ast.ast_processor.py

# -*- coding: utf-8 -*-
from antlr4 import FileStream, CommonTokenStream, ParseTreeWalker
from ast.JavaLexer import JavaLexer
from ast.JavaParser import JavaParser
import copy
import pprint
import unicodedata

source_encode = "utf-8"

class AstProcessor:

    def __init__(self, logging, listener):
        self.logging = logging
        self.logger = logging.getLogger(self.__class__.__name__)
        self.listener = listener

    def execute(self, input_source):
        file_stream = FileStream(input_source,encoding=source_encode)
        java_lexer = JavaLexer(file_stream)

        # CommonTokenStream へ渡す前であれば、commentを取得できます
        comments = self.extract_comments(java_lexer)

                
        # 一度、java_lexer.getAllTokens() を行うと
        # なぜか java_lexer が壊れるようですので、改めて
        # FileStream から。
        file_stream = FileStream(input_source,encoding=source_encode)
        java_lexer = JavaLexer(file_stream)
        
        common_token_stream = CommonTokenStream(java_lexer)
        parser = JavaParser(common_token_stream)
        walker = ParseTreeWalker()
        walker.walk(self.listener, parser.compilationUnit())

        ast_info = self.listener.ast_info

        # BasicInfoListener 内で getText()した場合、
        # 空白や改行がないtextが取得される為、ここで
        # CommonTokenStream から getText() します
        for tmp_class in ast_info['classes']:
            for method in tmp_class['methods']:
                start_index = method['body_pos']['start_index']
                stop_index  = method['body_pos']['stop_index']
            
                method['body_src'] = \
                    common_token_stream.getText(start_index,stop_index)
        
        return [ast_info, comments]

    def extract_comments(self, java_lexer):
        tmp_tokens = java_lexer.getAllTokens()
        comments = []

        # CommonTokenStream へ渡す前であれば、commentを取得できます
        for tmp_token in tmp_tokens:
            # LINE_COMMENT=110-1, IDENTIFIER=111-1 in JavaLexer.py
            if not tmp_token.type in [109,110]:
                continue
            comment_text = tmp_token.text
            comment_lines = comment_text.splitlines()
            stop_line = tmp_token.line + len(comment_lines)-1
            stop_column = len(comment_lines[-1])
            if tmp_token.line == stop_line:
                stop_column += tmp_token.column
                
            comments.append({
                "text":comment_text,
                "pos" :{"start_line"  :tmp_token.line,
                        "start_column":tmp_token.column,
                        "stop_line"   :stop_line,
                        "stop_column" :stop_column }
            })
        return comments

ast.basic_info_listener.py

# -*- coding: utf-8 -*-
from ast.JavaParserListener import JavaParserListener
from ast.JavaParser import JavaParser
import copy
import re
import sys
import pprint

class BasicInfoListener(JavaParserListener):
    def __init__(self):
        self.ast_info   = {'package'    : {},
                           'imports'    : [],
                           'classes'    : [] }
        self.class_base = {'name'       : '',
                           'annotation' : [],
                           'modifier'   : {},
                           'implements' : [],
                           'extends'    : '',
                           'fields'     : [],
                           'methods'    : [] }
        self.tmp_class = {}
        
        self.tmp_annotation = []
        self.tmp_modifier   = []


        
    def enterPackageDeclaration(self, ctx):
        self.ast_info['package'] = {
            'name' : ctx.qualifiedName().getText(),
            'pos'  : {'start_line'  : ctx.start.line,
                      'start_column': ctx.start.column,
                      'start_index' : ctx.start.tokenIndex,
                      'stop_line'   : ctx.stop.line,
                      'stop_column' : ctx.stop.column,
                      'stop_index'  : ctx.stop.tokenIndex}
                    }


    def enterImportDeclaration(self, ctx):
        self.ast_info['imports'].append(
            {'name' : ctx.qualifiedName().getText(),
             'pos'  : {'start_line'  : ctx.start.line,
                       'start_column': ctx.start.column,
                       'start_index' : ctx.start.tokenIndex,
                       'stop_line'   : ctx.stop.line,
                       'stop_column' : ctx.stop.column,
                       'stop_index'  : ctx.stop.tokenIndex}} )

    def enterClassOrInterfaceModifier(self, ctx):
        tmp_name = ctx.getText()
        # なぜか ctx.start.column == ctx.stop.column の為
        stop_column = ctx.stop.column + len(tmp_name)
        
        tmp_info = {
            'name' : tmp_name,
            'pos'  : {'start_line'  : ctx.start.line,
                      'start_column': ctx.start.column,
                      'start_index' : ctx.start.tokenIndex,
                      'stop_line'   : ctx.stop.line,
                      'stop_column' : stop_column,
                      'stop_index'  : ctx.stop.tokenIndex}}

        if re.match('^@', tmp_info['name']):
            self.tmp_annotation.append(tmp_info)
        else :
            self.tmp_modifier.append(tmp_info)
            
        
    def enterClassDeclaration(self, ctx):
        self.tmp_class = copy.copy( self.class_base )
        
        self.tmp_class['annotation'] = self.tmp_annotation
        self.tmp_class['modifier']   = self.tmp_modifier
        self.tmp_annotation = []
        self.tmp_modifier = []

        self.tmp_class['pos'] ={
            'start_line'  : ctx.start.line,
            'start_column': ctx.start.column,
            'start_index' : ctx.start.tokenIndex,
            'stop_line'   : ctx.stop.line,
            'stop_column' : ctx.stop.column,
            'stop_index'  : ctx.stop.tokenIndex }
        
        child_count = int(ctx.getChildCount())

        if child_count == 7:
            c1 = ctx.getChild(0)                        # class
            c2 = ctx.getChild(1).getText()              # class name
            c3 = ctx.getChild(2)                        # extends
            c4 = ctx.getChild(3).getChild(0).getText()  # extends class name
            c5 = ctx.getChild(4)                        # implements
            c7 = ctx.getChild(6)                        # class body
            
            self.tmp_class['name']       = c2
            self.tmp_class['extends']    = c4
            self.tmp_class['implements'] = \
                self.parse_implements_block(ctx.getChild(5))
            return
        
        if child_count == 5:
            c1 = ctx.getChild(0)                        # class
            c2 = ctx.getChild(1).getText()              # class name
            c3 = ctx.getChild(2).getText()              # extends or implements
            c5 = ctx.getChild(4)                        # class body

            self.tmp_class['name'] = c2
            if c3 == 'implements':
                self.tmp_class['implements'] = \
                    self.parse_implements_block(ctx.getChild(3))
            elif c3 == 'extends':
                c4 = ctx.getChild(3).getChild(0).getText()
                self.tmp_class['extends'] = c4
            return
        
        if child_count == 3:
            c1 = ctx.getChild(0)                        # class
            c2 = ctx.getChild(1).getText()              # class name
            c3 = ctx.getChild(2)                        # class body
            self.tmp_class['name'] = c2
            
            return
        
        print("ERROR unknown child_count"+ str(child_count))
        sys.exit()


    def exitClassDeclaration(self, ctx):
        self.ast_info['classes'].append(copy.copy(self.tmp_class) )
        
    
    def enterFieldDeclaration(self, ctx):
        field = {'type'      : ctx.getChild(0).getText(),
                 'body_src'  : ctx.getChild(1).getText(),
                 'annotation': [],
                 'modifier'  : []  }
        
        field['annotation'] = copy.copy(self.tmp_annotation)
        field['modifier']   = copy.copy(self.tmp_modifier)
        self.tmp_annotation = []
        self.tmp_modifier = []
        
        self.tmp_class['fields'].append(field)


    def enterMethodDeclaration(self, ctx):
        
        c1 = ctx.getChild(0).getText()  # return type
        c2 = ctx.getChild(1).getText()  # method name
        # params
        params = self.parse_method_params_block(ctx.getChild(2))

        # method bodyを CommonTokenStream と tokenIndex により得る為
        ctx_method_body = ctx.getChild(-1)


        method_info = {'returnType': c1,
                       'name'      : c2,
                       'annotation': [],
                       'modifier'  : [],
                       'params': params,
                       'pos'  : {'start_line'  : ctx.start.line,
                                 'start_column': ctx.start.column,
                                 'start_index' : ctx.start.tokenIndex,
                                 'stop_line'   : ctx.stop.line,
                                 'stop_column' : ctx.stop.column,
                                 'stop_index'  : ctx.stop.tokenIndex},
                       'body_pos' : {
                           'start_line'  : ctx_method_body.start.line,
                           'start_column': ctx_method_body.start.column,
                           'start_index' : ctx_method_body.start.tokenIndex,
                           'stop_line'   : ctx_method_body.stop.line,
                           'stop_column' : ctx_method_body.stop.column,
                           'stop_index'  : ctx_method_body.stop.tokenIndex}}
        method_info['annotation'] = self.tmp_annotation
        method_info['modifier']   = self.tmp_modifier
        self.tmp_annotation = []
        self.tmp_modifier = []

        self.tmp_class['methods'].append(method_info)

           
    def parse_implements_block(self, ctx):
        implements_child_count = int(ctx.getChildCount())
        result = []
        if implements_child_count == 1:
            impl_class = ctx.getChild(0).getText()
            result.append(impl_class)
        elif implements_child_count > 1:
            for i in range(implements_child_count):
                if i % 2 == 0:
                    impl_class = ctx.getChild(i).getText()
                    result.append(impl_class)
        return result

    def parse_method_params_block(self, ctx):
        params_exist_check = int(ctx.getChildCount())
        result = []
        if params_exist_check == 3:
            params_child_count = int(ctx.getChild(1).getChildCount())
            if params_child_count == 1:
                param_type = ctx.getChild(1).getChild(0).getChild(0).getText()
                param_name = ctx.getChild(1).getChild(0).getChild(1).getText()
                param_info = {'paramType': param_type,
                              'paramName': param_name }
                result.append(param_info)
            elif params_child_count > 1:
                for i in range(params_child_count):
                    if i % 2 == 0:
                        param_type = \
                            ctx.getChild(1).getChild(i).getChild(0).getText()
                        param_name = \
                            ctx.getChild(1).getChild(i).getChild(1).getText()
                        param_info = {'paramType': param_type,
                                      'paramName': param_name }
                        result.append(param_info)
        return result

再 - antlr4-python3-runtime for python3 による java source の parse / 構文解析

antlr4-python3-runtime for python3 による java source の parse / 構文解析 - end0tknr's kipple - web写経開発

以前、記載した上記entry の修正版。

*.java には、複数classの定義も可能ですので、以下を修正。

上記以外は、前回のentryの通りですので、修正後

$ /usr/local/python3/bin/python3 ast_analyze_executor.py \
    path/to/src/hoge/BaseAction.java

のように、実行して下さい。

ast_analyze_executor.py

# -*- coding: utf-8 -*-

import os
import sys
sys.path.append( os.path.dirname(__file__) )

import logging.config
from ast.ast_processor import AstProcessor
from ast.basic_info_listener import BasicInfoListener
import sys
import yaml
import pprint

log_conf = './log_conf.yaml'  # log設定は自分でも怪しいと思う

def main():
    logging.config.dictConfig(yaml.load(open(log_conf).read(),
                                        Loader=yaml.SafeLoader))
    logger = logging.getLogger('mainLogger')

    target_file_path = sys.argv[1]
    ast_info = \
        AstProcessor(logging, BasicInfoListener()).execute(target_file_path)

    print(pprint.pformat(ast_info, width=80)) # 幅:80文字に整形
#    pprint.pprint(ast_info)


if __name__ == "__main__":
    main()
    

ast.ast_processor.py

# -*- coding: utf-8 -*-
from antlr4 import FileStream, CommonTokenStream, ParseTreeWalker
from ast.JavaLexer import JavaLexer
from ast.JavaParser import JavaParser
import pprint

source_encode = "utf-8"

class AstProcessor:

    def __init__(self, logging, listener):
        self.logging = logging
        self.logger = logging.getLogger(self.__class__.__name__)
        self.listener = listener

    def execute(self, input_source):
        file_stream = FileStream(input_source,encoding=source_encode)
        common_token_stream = CommonTokenStream(JavaLexer(file_stream))
        
        
        parser = JavaParser(common_token_stream)
        walker = ParseTreeWalker()
        walker.walk(self.listener, parser.compilationUnit())

        ast_info = self.listener.ast_info

        # BasicInfoListener 内で getText()した場合、
        # 空白や改行がないtextが取得される為、ここで
        # CommonTokenStream から getText() します
        
        for tmp_class in ast_info['classes']:
            for method in tmp_class['methods']:
                start_index = method['body_pos']['start_index']
                stop_index  = method['body_pos']['stop_index']
            
                method['body_src'] = \
                    common_token_stream.getText(start_index,stop_index)
        
        return ast_info

ast.basic_info_listener.py

# -*- coding: utf-8 -*-
from ast.JavaParserListener import JavaParserListener
from ast.JavaParser import JavaParser
import copy
import re
import sys
import pprint

class BasicInfoListener(JavaParserListener):
    def __init__(self):
        self.ast_info   = {'package'    : {},
                           'imports'    : [],
                           'classes'    : [] }
        self.class_base = {'name'       : '',
                           'annotation' : [],
                           'modifier'   : {},
                           'implements' : [],
                           'extends'    : '',
                           'fields'     : [],
                           'methods'    : [] }
        self.tmp_class = {}
        
        self.tmp_annotation = []
        self.tmp_modifier   = []

    def enterPackageDeclaration(self, ctx):
        self.ast_info['package'] = {
            'name' : ctx.qualifiedName().getText(),
            'pos'  : {'start_line'  : ctx.start.line,
                      'start_column': ctx.start.column,
                      'start_index' : ctx.start.tokenIndex,
                      'stop_line'   : ctx.stop.line,
                      'stop_column' : ctx.stop.column,
                      'stop_index'  : ctx.stop.tokenIndex}
                    }


    def enterImportDeclaration(self, ctx):
        self.ast_info['imports'].append(
            {'name' : ctx.qualifiedName().getText(),
             'pos'  : {'start_line'  : ctx.start.line,
                       'start_column': ctx.start.column,
                       'start_index' : ctx.start.tokenIndex,
                       'stop_line'   : ctx.stop.line,
                       'stop_column' : ctx.stop.column,
                       'stop_index'  : ctx.stop.tokenIndex}} )

    def enterClassOrInterfaceModifier(self, ctx):
        tmp_name = ctx.getText()
        # なぜか ctx.start.column == ctx.stop.column の為
        stop_column = ctx.stop.column + len(tmp_name)
        
        tmp_info = {
            'name' : tmp_name,
            'pos'  : {'start_line'  : ctx.start.line,
                      'start_column': ctx.start.column,
                      'start_index' : ctx.start.tokenIndex,
                      'stop_line'   : ctx.stop.line,
                      'stop_column' : stop_column,
                      'stop_index'  : ctx.stop.tokenIndex}}

        if re.match('^@', tmp_info['name']):
            self.tmp_annotation.append(tmp_info)
        else :
            self.tmp_modifier.append(tmp_info)
            
        
    def enterClassDeclaration(self, ctx):
        self.tmp_class = copy.copy( self.class_base )
        
        self.tmp_class['annotation'] = self.tmp_annotation
        self.tmp_class['modifier']   = self.tmp_modifier
        self.tmp_annotation = []
        self.tmp_modifier = []

        self.tmp_class['pos'] ={
            'start_line'  : ctx.start.line,
            'start_column': ctx.start.column,
            'start_index' : ctx.start.tokenIndex,
            'stop_line'   : ctx.stop.line,
            'stop_column' : ctx.stop.column,
            'stop_index'  : ctx.stop.tokenIndex }
        
        child_count = int(ctx.getChildCount())

        if child_count == 7:
            c1 = ctx.getChild(0)                        # class
            c2 = ctx.getChild(1).getText()              # class name
            c3 = ctx.getChild(2)                        # extends
            c4 = ctx.getChild(3).getChild(0).getText()  # extends class name
            c5 = ctx.getChild(4)                        # implements
            c7 = ctx.getChild(6)                        # class body
            
            self.tmp_class['name']       = c2
            self.tmp_class['extends']    = c4
            self.tmp_class['implements'] = \
                self.parse_implements_block(ctx.getChild(5))
            return
        
        if child_count == 5:
            c1 = ctx.getChild(0)                        # class
            c2 = ctx.getChild(1).getText()              # class name
            c3 = ctx.getChild(2).getText()              # extends or implements
            c5 = ctx.getChild(4)                        # class body

            self.tmp_class['name'] = c2
            if c3 == 'implements':
                self.tmp_class['implements'] = \
                    self.parse_implements_block(ctx.getChild(3))
            elif c3 == 'extends':
                c4 = ctx.getChild(3).getChild(0).getText()
                self.tmp_class['extends'] = c4
            return
        
        if child_count == 3:
            c1 = ctx.getChild(0)                        # class
            c2 = ctx.getChild(1).getText()              # class name
            c3 = ctx.getChild(2)                        # class body
            self.tmp_class['name'] = c2
            
            return
        
        print("ERROR unknown child_count"+ str(child_count))
        sys.exit()


    def exitClassDeclaration(self, ctx):
        self.ast_info['classes'].append(copy.copy(self.tmp_class) )
        
    
    def enterFieldDeclaration(self, ctx):
        field = {'type'      : ctx.getChild(0).getText(),
                 'body_src'  : ctx.getChild(1).getText(),
                 'annotation': [],
                 'modifier'  : []  }
        
        field['annotation'] = copy.copy(self.tmp_annotation)
        field['modifier']   = copy.copy(self.tmp_modifier)
        self.tmp_annotation = []
        self.tmp_modifier = []
        
        self.tmp_class['fields'].append(field)


    def enterMethodDeclaration(self, ctx):
        
        c1 = ctx.getChild(0).getText()  # return type
        c2 = ctx.getChild(1).getText()  # method name
        # params
        params = self.parse_method_params_block(ctx.getChild(2))

        # method bodyを CommonTokenStream と tokenIndex により得る為
        ctx_method_body = ctx.getChild(-1)


        method_info = {'returnType': c1,
                       'name'      : c2,
                       'annotation': [],
                       'modifier'  : [],
                       'params': params,
                       'pos'  : {'start_line'  : ctx.start.line,
                                 'start_column': ctx.start.column,
                                 'start_index' : ctx.start.tokenIndex,
                                 'stop_line'   : ctx.stop.line,
                                 'stop_column' : ctx.stop.column,
                                 'stop_index'  : ctx.stop.tokenIndex},
                       'body_pos' : {
                           'start_line'  : ctx_method_body.start.line,
                           'start_column': ctx_method_body.start.column,
                           'start_index' : ctx_method_body.start.tokenIndex,
                           'stop_line'   : ctx_method_body.stop.line,
                           'stop_column' : ctx_method_body.stop.column,
                           'stop_index'  : ctx_method_body.stop.tokenIndex}}
        method_info['annotation'] = self.tmp_annotation
        method_info['modifier']   = self.tmp_modifier
        self.tmp_annotation = []
        self.tmp_modifier = []

        self.tmp_class['methods'].append(method_info)

           
    def parse_implements_block(self, ctx):
        implements_child_count = int(ctx.getChildCount())
        result = []
        if implements_child_count == 1:
            impl_class = ctx.getChild(0).getText()
            result.append(impl_class)
        elif implements_child_count > 1:
            for i in range(implements_child_count):
                if i % 2 == 0:
                    impl_class = ctx.getChild(i).getText()
                    result.append(impl_class)
        return result

    def parse_method_params_block(self, ctx):
        params_exist_check = int(ctx.getChildCount())
        result = []
        if params_exist_check == 3:
            params_child_count = int(ctx.getChild(1).getChildCount())
            if params_child_count == 1:
                param_type = ctx.getChild(1).getChild(0).getChild(0).getText()
                param_name = ctx.getChild(1).getChild(0).getChild(1).getText()
                param_info = {'paramType': param_type,
                              'paramName': param_name }
                result.append(param_info)
            elif params_child_count > 1:
                for i in range(params_child_count):
                    if i % 2 == 0:
                        param_type = \
                            ctx.getChild(1).getChild(i).getChild(0).getText()
                        param_name = \
                            ctx.getChild(1).getChild(i).getChild(1).getText()
                        param_info = {'paramType': param_type,
                                      'paramName': param_name }
                        result.append(param_info)
        return result

jquery & php による jsonp オレオレ template

たまに実装すると、すっかり忘れていることを痛感するので、メモ。

以下では snippet src しか記載していませんが、詳細は察して下さい。

client側 ( jquery / javascript )

FormBase.prototype = {
    api_url: "/cgi-bin3/form.php",
    err_url: "/contact3/common/error.html",
    err_msg_elm: "#callback_error",
    
    // 入力→確認画面遷移に伴う入力確認
    submit_input_to_confirm: function(){
        $(this.err_msg_elm).empty();

        form_data = this.collect_form_datas();
        var this_obj = this;
        
        var request = $.ajax({
            type: 'POST',
            url: this.api_url,
            data: form_data,
            dataType: 'jsonp',
            jsonpCallback: 'callback_input_to_confirm'
        });

        request.done(function(data,text_status){
            return this_obj.callback_input_to_confirm(data);
        });

        request.fail(function(jqXHR, textStatus, errorThrown) {
            window.onbeforeunload = null; //「離れていいですか?」をOFF
            $(window).off('beforeunload');
            window.location.href = this_obj.err_url;
        });
    },
    
    callback_input_to_confirm:function(submit_result){
        // OK→ セッションNoを受領し、確認画面を表示
        if(submit_result["result"]=="OK"){
            this.session_id = submit_result["session_id"];
            return this.init_confirm_page();
        }
        this.session_id = '';

        // NG→ 画面遷移せず、エラー内容を表示
        if("errors" in submit_result ){
            for (var atri_key in submit_result["errors"] ) {
                for (var i=0; i<submit_result["errors"][atri_key].length; i++){
                    var err_msg = submit_result["errors"][atri_key][i];
                    
                    $(this.err_msg_elm).append('<li>'+ err_msg +'</li>');
                }
            }
            $('html').animate( {scrollTop: $('.flow').offset().top - 50} );
        }
    },
}

server側 ( php )

class HelmForm4Client extends HelmForm {
    const JSONP_RES_HEADER = 'Content-Type: text/javascript; charset=utf-8';
    
    function __construct() {
        $this->ini_set();
    }

    function main(){
        $func_name = __CLASS__." ".__FUNCTION__;

        $refferer  = $_SERVER['HTTP_REFERER'];
        $parsed_url = parse_url($refferer);

        $this->write_log("START $func_name from $refferer");

        $assign_form_def = $this->assign_form_def($parsed_url['host'],
                                                  $parsed_url['path']);

        // request元からのaccess可否判定
        if( ! $assign_form_def ){
            $this->write_log("ERROR not assigned foom: $refferer");
            
            http_response_code( 500 );
            header(self::JSONP_RES_HEADER);
            echo "({'result':'NG',sys_msg':'bad request'});";
            return;
        }
        
        $callback_method = $_GET["callback"];

        //入力画面→確認画面 遷移時
        if(strcmp($callback_method,"callback_input_to_confirm")==0){
            return $this->main_input_to_confirm($callback_method,
                                                $assign_form_def);
        }
        //確認画面→完了画面 遷移時
        if(strcmp($callback_method,"callback_confirm_to_complete")==0){
            $result_main = $this->main_confirm_to_complete($callback_method,
                                                           $assign_form_def);
            return $result_main;
        }
        
        http_response_code( 500 );
        $this->write_log("WARN unknown request ". $_SERVER['REQUEST_URI'] );
    }

    
    //入力画面→確認画面 遷移時
    function main_input_to_confirm($callback_method, $assign_form_def){
        $func_name = __CLASS__." ".__FUNCTION__;
        $refferer = $assign_form_def['path'];
    
        // session id設定とsession開始
        session_id(sha1(uniqid(microtime())));
        if( session_start() ){
            $session_id = session_id();
        }

        $this->write_log("START $func_name session:$session_id $refferer");
        
        
        // validation ruleのload
        $validation_def
            = $this->validation_def($assign_form_def['validation_rule']);
        
        // request parameterの取得とvalidation
        $params_defs = $validation_def["definition"];

        // 住所、氏名等のpost reqesut parameterを取得
        $req_params_tmp = $this->load_req_params($params_defs, $_POST);
        $req_params = $req_params_tmp[0];
        $errors     = $req_params_tmp[1];

        // カタログに関する post reqesut parameterを取得
        $catalog = new HelmForm4Catalog();
        $max_catalogs = $assign_form_def["max_catalogs"];
        $req_params_tmp = $catalog->load_req_params($_POST,$max_catalogs);
        $req_params = array_merge($req_params, $req_params_tmp[0]);
        $errors     = array_merge($errors,     $req_params_tmp[1]);

        $_SESSION['req_params'] = $req_params;

        //validationの結果、errorなしなら、session idをclientへ返します
        if( count($errors)==0 ){
            $req_result_json = $this->json_encode(["result"    =>"OK",
                                                   "session_id"=>$session_id]);
            echo $callback_method ."(".$req_result_json .");";
            $this->write_log("DONE $func_name session:$session_id");
            return true;
        }
    
        $this->write_log("WARN fail validation $func_name session:$session_id");

        $tmp_msg = "validation result:". print_r($errors,true);
        // $tmp_msg = str_replace ("\n"," ",$tmp_msg);
        $tmp_msg = preg_replace("/\s+/"," ",$tmp_msg);
        $this->write_log($tmp_msg);

        $req_result_json = $this->json_encode(["result"=>"NG","errors"=>$errors]);
        $callback_method_all = $callback_method ."(".$req_result_json .");";
        header(self::JSONP_RES_HEADER);
        echo $callback_method_all;
    
        // セッション情報破棄
        $_SESSION = [];
        session_destroy();
        return false;
    }

}

phpの adodb によるmysql / postgres接続練習

phpのPDOによるmysql / postgres接続練習 - end0tknr's kipple - web写経開発

以前の上記entry にて、PDO 版を記載しましたが。 今回は、adodb版。

adodb for php は、以下のurl よりダウンロードした zipを解凍するだけで利用できます。

mysqlの場合

<?php
require ('adodb5/adodb.inc.php');
 
$sqltype  = "mysql";
$server   = "localhost";
$user     = "root";
$password = "ないしょ";
$dbname   = "test";
 
$db = NewADOConnection($sqltype);

$db->pconnect($server, $user, $password, $dbname);
if(! $db->isConnected() ){
    die("failt pconnect()");
}

$id_val = $db->param('id');

$sql = "select * from test_tbl where id=?";
$bind_vars = ["id"=>1];
$result = $db->execute($sql,$bind_vars);

while ($row = $result->fetchRow() ) {
    echo print_r($row,true);
    //$db->ErrorMsg()."\n";
}

?>

↑こう書くと、↓こう表示されます

$ php foo3.php 
Array(
    [0] => 1
    [id] => 1
    [1] => VAL1
    [val] => VAL1)

postgresの場合

<?php
require ('adodb5/adodb.inc.php');
 
$sqltype  = "postgres";
$server   = "localhost";
$user     = "ないしょ";
$password = "ないしょ";
$dbname   = "ないしょ";
 
$db = NewADOConnection($sqltype);

$db->pconnect($server, $user, $password, $dbname);
if(! $db->isConnected() ){
    die("failt pconnect()");
}

$id_val = $db->param('id');

$sql = "select * from h_prj01_table0001 where ext61=?";
$bind_vars = ["ext61"=>"品質"];
$result = $db->execute($sql,$bind_vars);

$i = 0;
while ($row = $result->fetchRow() ) {
    $i ++;
    echo "$i\n";
    echo print_r($row,true);
    //$db->ErrorMsg()."\n";
}

?>

実行後、表示される内容は、先程のmysqlと同様ですので、省略します。

PHPMD による php source の 循環複雑度計測( Cyclomatic complexity code metrics)

PhpMetrics による php source の 循環複雑度計測( Cyclomatic complexity code metrics) - end0tknr's kipple - web写経開発

php の code metrics は、上記のentry で PhpMetricsによるものを記載していますが、 PhpMetrics や、PHP Depend では、計測できないことがありました。

計測できない原因は不明でしたが、PHPMD では計測できましたので、 以下では PHPMD の使用法をメモしておきます。

install by Composer

https://pdepend.org/download/index.html

上記urlによれば、PHPMDのインストール方法には Composer や git clone 等、複数ありますが、 以前の PhpMetrics が Composer でしたので、今回も利用します。

まぁ、以下のインストール方法は、上記urlの通りです。

また、PHPMDは、別のmetrics ツールである PHP Depend に依存しているらしく これも併せて、vender以下にインストールされます。

$ vi composer.json 
{"require": { "pdepend/pdepend" : "@stable" }}

$ curl -s http://getcomposer.org/installer | php
$ php composer.phar install


$ ./vendor/bin/phpmd --help
Mandatory arguments:
1) A php source code filename or directory. Can be a comma-separated string
2) A report format
3) A ruleset filename or a comma-separated string of rulesetfilenames

Example: phpmd /path/to/source format ruleset

Available formats: ansi, html, json, text, xml.
Available rulesets: cleancode, codesize, controversial, design, naming, unusedcode.

Optional arguments that may be put after the mandatory arguments:
--minimumpriority: rule priority threshold;
                   rules with lower priority than this will not be used
--reportfile: send report output to a file; default to STDOUT
--suffixes: comma-separated string of valid source code filename extensions,
            e.g. php,phtml
--exclude: comma-separated string of patterns that are used to ignore directories.
           Use asterisks to exclude by pattern. For example *src/foo/*.php or *src/foo/*
--strict: also report those nodes with a @SuppressWarnings annotation
--ignore-violations-on-exit: will exit with a zero code, even if any violations are found

code metrics を計測してみる

「phpmd --help」で分かるように PHPMDは 「$ phpmd /path/to/source format ruleset」のように実行します。

ちなみに、ruleset には、以下があるようです。

RULE NOTE
cleancode 整形ルール
codesize コードサイズと複雑度
controversial 賛否両論のあるものの為、検証対象から除外も可
design ソフトウェアデザインというより、コーディングルール
naming 命名規則
unusedcode 未使用コードチェック

で、以下の通りです。

$ ./vendor/bin/phpmd ~/tmp/form/cgi-bin2 text cleancode,codesize

cgi-bin2/form_api.php:122       The function main_confirm_to_complete() has a Cyclomatic Complexity of 15.
                                The configured cyclomatic complexity threshold is 10.
cgi-bin2/form_api.php:122       The function main_confirm_to_complete() has an NPath complexity of 440.
                                The configured NPath complexity threshold is 200.
cgi-bin2/form_api.php:122       The function main_confirm_to_complete() has 238 lines of code.
                                Current threshold is set to 100. Avoid really long methods.
cgi-bin2/form_api.php:223       Avoid using undefined variables such as '$db_name' which will lead to PHP notices.
cgi-bin2/form_api.php:223       Avoid using undefined variables such as '$form_id' which will lead to PHP notices.
cgi-bin2/form_api.php:228       Avoid using undefined variables such as '$save_path' which will lead to PHP notices.
cgi-bin2/form_api.php:233       Avoid using undefined variables such as '$mailfromaddress' which will lead to PHP notices.
cgi-bin2/form_api.php:235       Avoid using undefined variables such as '$mailfromaddress' which will lead to PHP notices.
cgi-bin2/form_api.php:236       Avoid using undefined variables such as '$systemmail' which will lead to PHP notices.
    :                               :

SpringBoot for java における validation練習

@Required(msg = @Msg(key = "errors.required"),
                     arg0 = @Arg(key = "LOGIN ID", resource = false))
@Maxbytelength(maxbytelength=20,
               msg =@Msg(key = "errors.maxbytelength"),
                         arg0 = @Arg(key = "LOGIN ID", resource = false),
                 arg1 = @Arg(key = "20", resource = false))
public String loginId;

SEASAR2 では、上記のように validation を実施していましたが、 このまま Spring Boot へ移植できないものかと思い、試し始めました。

試した結果、なんとなく理解しましたが、今後、気が向いたら、以下のTODOを調べます。

  • {0} の記載で、エラーメッセージにフィールド名を埋め込めるようです。(試したが不明)
  • 各明細行等の為に、ネストなvalidationがある。(試してない)
  • 更に複雑なルールには、独自validation作成が必要。(試してない)

上記以外のポイントは、各srcのcommentに記載しています。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.4</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>jp.end0tknr</groupId>
  <artifactId>MySpring</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>MySpring</name>
  <description>Bukkenkoutei written from SEASAR2 to SpringBoot</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
        <dependency>
      <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
          <exclusion><!--【以下はLOG4J2の為】-->
             <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
          </exclusion>
        </exclusions>
    </dependency>
        <dependency><!--【LOMBOK】 -->
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    <dependency><!--【for JSP】 -->
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
    </dependency>
    <dependency> <!--【VALIDATION】 -->
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency><!--【ログ出力は LOG4J2】-->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
        <dependency><!--【今回は CustomeStringTrimmerEditor より参照】-->
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

src/main/resources/application.properties

# 昔ながらのJSPを使用する為
spring.mvc.view.prefix= /WEB-INF/view/
spring.mvc.view.suffix= .jsp

# MessageSource が参照するfileを指定
spring.messages.basename=messages,validation_messages
spring.messages.cache-seconds=-1
spring.messages.encoding=UTF-8

src/main/resources/log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
  <Properties>
    <Property name="app_name">demo</Property>
    <Property name="date">%d{yyyy-MM-dd HH:mm:ss.SSS}</Property>
    <Property name="daily_log">logs/app${app_name}_%d{yyyy-MM-dd}.log</Property>
  </Properties>
  <appenders>
    <Console
      name="Console"
      target="SYSTEM_OUT"
    >
      <PatternLayout
       pattern="${date}, [${app_name}], [ %-5level ], %logger{10}, %msg %n" />
    </Console>
    <RollingFile
      name="File"
      fileName="logs/app.log"
      filePattern="${daily_log}.gz">
      <PatternLayout
       pattern="${date}, [${app_name}], [ %-5level ], %logger{10}, %msg %n" />
      <Policies>
        <TimeBasedTriggeringPolicy />
      </Policies>
    </RollingFile>
  </appenders>
  <loggers>
    <root level="info">
      <appender-ref ref="Console" />
      <appender-ref ref="File" />
    </root>
  </loggers>
</configuration>

src/main/resources/messages_ja.properties

messages.properties も、空ファイルで必要

login.pleaseinput= 名前を入力してください
honorific.title = {0}さん

src/main/resources/validation_messages_ja.properties

validation_messages.properties も、空ファイルで必要

# {0}による フィールド名埋め込み方法は不明でした
javax.validation.constraints.NotNull.message= {0} は必須項目です
javax.validation.constraints.Size.message= {0}は {min} ~ {max} 文字で入力ください
# EL式により、以下のようにも記載できます。
javax.validation.constraints.Size.message_el = \
   ${min==max ? min += '文字で入力ください' : \
     min==1   ? max += '文字以内で入力ください' : \
                min += '~' += max += '文字で入力ください'}

src/main/webapp/WEB-INF/view/login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form method="post" action="/login">

<hr/>
${msg}
<hr/>

<div>LOGIN ID 1<input type="text" name="loginId1"></div>
<div>LOGIN ID 2<input type="text" name="loginId2"></div>
<div>LOGIN ID 3<input type="text" name="loginId3"></div>

<button type="submit">SUBMIT</button>
</form>
</body>
</html>

src/main/java/jp.end0tknr/MySpringApplication.java

package jp.end0tknr;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;


@SpringBootApplication
public class MySpringApplication {

    public static void main(String[] args) {
        SpringApplication.run(MySpringApplication.class, args);
    }

    // validation error 時のメッセージを
    // validation_messages.properties ファイルから参照する為
    @Autowired
    private MessageSource msgSrc;
    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalValidatorFactoryBean localValidatorFactoryBean
        = new LocalValidatorFactoryBean();

        localValidatorFactoryBean.setValidationMessageSource(msgSrc);
        return localValidatorFactoryBean;
    }
}

src/main/java/jp.end0tknr/CustomeStringTrimmerEditor.java

package jp.end0tknr;

import java.beans.PropertyEditorSupport;

import org.apache.commons.lang3.StringUtils;

// <input type="text">からpostされた値を全角空白含め、
// 予め、trimする為のclassです
public class CustomeStringTrimmerEditor extends PropertyEditorSupport {
    private final boolean emptyAsNull;

    public CustomeStringTrimmerEditor(boolean emptyAsNull) {
        this.emptyAsNull = emptyAsNull;
    }

    @Override
    public String getAsText() {
        Object value = this.getValue();
        return value != null ? value.toString() : "";
    }

    @Override
    public void setAsText(String text) {
        if (text == null) {
            this.setValue((Object) null);
        } else {
            String value = StringUtils.strip(text);

            if (this.emptyAsNull && "".equals(value)) {
                this.setValue((Object) null);
            } else {
                this.setValue(value);
            }
        }
    }

}

src/main/java/jp.end0tknr/LoginAction.java

package jp.end0tknr;

import java.util.Locale;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class LoginAction {

    static protected Logger logger = LogManager.getLogger(LoginAction.class);
    @Autowired
    MessageSource msgsrc;


    @RequestMapping(value="/login", method=RequestMethod.GET)
    public ModelAndView index(ModelAndView modelview) {

        // messages.properties 内のメッセージへの値の埋め込み
        String msg_1 = msgsrc.getMessage(
                "honorific.title", new String[]{"ゲスト"}, Locale.JAPAN);
        String msg_2 = msgsrc.getMessage(
                "login.pleaseinput", null, Locale.JAPAN);

        // jsp にある ${msg} へ埋め込み
        modelview.addObject("msg",msg_1 +" "+msg_2);

        modelview.setViewName("login");
        return modelview;
    }

    @RequestMapping(value="/login", method=RequestMethod.POST)
    public ModelAndView send(
            @Validated LoginForm loginForm,
            BindingResult errors,
            ModelAndView modelview) {

        logger.info("START This method.");

        String msg = "";

        // validation errorがある場合、メッセージ等を取得
        if(errors.hasErrors()){
            logger.error("This method has error.");

            for (FieldError fieldErr : errors.getFieldErrors()) {
                msg += "Field:" + fieldErr.getField();
                msg += " x Code:" + fieldErr.getCode();
                msg += " x Msg:" + fieldErr.getDefaultMessage();
                msg += "<hr/>";
            }
        }

        logger.info("MSG" + msg);

        modelview.addObject("msg",msg);
        modelview.setViewName("login");
        return modelview;
    }


}

src/main/java/jp.end0tknr/LoginForm.java

package jp.end0tknr;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LoginForm {
    @Autowired
    MessageSource msgsrc;

    @NotNull(message="{javax.validation.constraints.NotNull.message}")
    @Size(min=1, max=3, message="{0}は、{min}~{max}文字で入力してください")
    public String loginId1;

    @NotNull
    @Size(min=1, max=3, message="{javax.validation.constraints.Size.message}")
    public String loginId2;

    @NotNull
    @Size(min=1, max=3, message="{javax.validation.constraints.Size.message_el}")
    public String loginId3;
}

log4j2 for java による SprintBoot2 へのログ出力

以下のurlの写経。

こばやしたかはるblog: SpringでLog4j2

いつも思いますが、ログ出力って、ごく単純な機能なのに、 なんで、こんなにもPG言語やライブラリ毎に利用法に違いがあるんでしょ?

毎度、使用方法を忘れている気がします。

STEP 1 - pom.xml の編集

以下にPOM全体を記載していますが、 「spring-boot-starter-logging」「spring-boot-starter-log4j2」あたりを追記。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.4</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>jp.co.sexy.spak2</groupId>
  <artifactId>SpakSpring</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>SpakSpring</name>
  <description>Spak written from SEASAR2 to SpringBoot</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
        <dependency>
      <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
          <!-- LOG4J2 のよる出力の為、以下を追加 -->
          <exclusion>
             <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
          </exclusion>
        </exclusions>
      </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- HTML TEMPLATEは thymeleafでなく、昔ながらのjsp -->
    <dependency>
       <groupId>org.apache.tomcat.embed</groupId>
         <artifactId>tomcat-embed-jasper</artifactId>
    </dependency>
    <!-- ログ出力は、LOG4J2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

STEP 2 - src/main/resources/log4j2.xml の作成

参考にさせて頂いたurlでは、様々設定されていますが、 今回、とりあえず出力するだけですので、以下の程度です。

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
    <Properties>
        <Property name="app_name">demo</Property>
        <Property name="date">%d{yyyy-MM-dd HH:mm:ss.SSS}</Property>
        <Property name="daily_log">logs/app${app_name}_%d{yyyy-MM-dd}.log</Property>
    </Properties>
    <appenders>
        <Console
            name="Console"
            target="SYSTEM_OUT"
        >
            <PatternLayout pattern="${date}, [${app_name}], [ %-5level ], %logger{10}, %msg %n" />
        </Console>
        <RollingFile
            name="File"
            fileName="logs/app.log"
            filePattern="${daily_log}.gz">
            <PatternLayout pattern="${date}, [${app_name}], [ %-5level ], %logger{10}, %msg %n" />
            <Policies>
                <TimeBasedTriggeringPolicy />
            </Policies>
        </RollingFile>
    </appenders>
    <loggers>
        <root level="info">
            <appender-ref ref="Console" />
            <appender-ref ref="File" />
        </root>
    </loggers>
</configuration>

STEP 3 - *.java での出力処理

「★」の部分を追記します。

package jp.end0tknr.action;

import org.apache.logging.log4j.LogManager; // ★
import org.apache.logging.log4j.Logger;     // ★
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;


@Controller
public class IndexAction {
    // ★
    static protected Logger logger = LogManager.getLogger(IndexAction.class);

    @RequestMapping("/")
    public String index(Model model) {
        logger.info("start index()"); // ★ 
        return "index";
    }

}

STEP4 - 出力確認

以上により、コンソール & $APP_ROOT/logs/app.log へ、 以下のように出力されます。

2021-03-20 15:18:50.894, [demo], [ INFO  ], jp.co.sexy.spak.SpakSpringApplication, Starting SpakSpringApplication using Java 1.8.0_202 on LAPTOP-Q767CC9I with PID 23104 (C:\Users\end0t\workspace_eclipse_2020\SpakSpring\target\classes started by end0t in C:\Users\end0t\workspace_eclipse_2020\SpakSpring) 
2021-03-20 15:18:50.976, [demo], [ INFO  ], jp.co.sexy.spak.SpakSpringApplication, No active profile set, falling back to default profiles: default 
2021-03-20 15:18:52.103, [demo], [ INFO  ], org.springframework.boot.web.embedded.tomcat.TomcatWebServer, Tomcat initialized with port(s): 8080 (http) 
2021-03-20 15:18:52.125, [demo], [ INFO  ], org.apache.coyote.http11.Http11NioProtocol, Initializing ProtocolHandler ["http-nio-8080"] 
2021-03-20 15:18:52.126, [demo], [ INFO  ], org.apache.catalina.core.StandardService, Starting service [Tomcat] 
2021-03-20 15:18:52.127, [demo], [ INFO  ], org.apache.catalina.core.StandardEngine, Starting Servlet engine: [Apache Tomcat/9.0.44] 
2021-03-20 15:18:52.340, [demo], [ INFO  ], org.apache.jasper.servlet.TldScanner, At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time. 
2021-03-20 15:18:52.345, [demo], [ INFO  ], org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/], Initializing Spring embedded WebApplicationContext 
2021-03-20 15:18:52.345, [demo], [ INFO  ], org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext, Root WebApplicationContext: initialization completed in 1294 ms 
2021-03-20 15:18:52.568, [demo], [ INFO  ], org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor, Initializing ExecutorService 'applicationTaskExecutor' 
2021-03-20 15:18:52.682, [demo], [ INFO  ], org.springframework.boot.autoconfigure.web.servlet.WelcomePageHandlerMapping, Adding welcome page template: index 
2021-03-20 15:18:52.806, [demo], [ INFO  ], org.apache.coyote.http11.Http11NioProtocol, Starting ProtocolHandler ["http-nio-8080"] 
2021-03-20 15:18:52.850, [demo], [ INFO  ], org.springframework.boot.web.embedded.tomcat.TomcatWebServer, Tomcat started on port(s): 8080 (http) with context path '' 
2021-03-20 15:18:52.985, [demo], [ INFO  ], jp.co.sexy.spak.SpakSpringApplication, Started SpakSpringApplication in 2.632 seconds (JVM running for 5.894) 
2021-03-20 15:19:11.227, [demo], [ INFO  ], org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/], Initializing Spring DispatcherServlet 'dispatcherServlet' 
2021-03-20 15:19:11.227, [demo], [ INFO  ], org.springframework.web.servlet.DispatcherServlet, Initializing Servlet 'dispatcherServlet' 
2021-03-20 15:19:11.228, [demo], [ INFO  ], org.springframework.web.servlet.DispatcherServlet, Completed initialization in 1 ms 
2021-03-20 15:19:11.268, [demo], [ INFO  ], jp.co.sexy.spak.action.IndexAction, start index() 

python unittestのオレオレ 雛形(テンプレート template )

python srcのオレオレ 雛形(テンプレート template ) - end0tknr's kipple - web写経開発

上記の unittest 版です

#!python
# -*- coding: utf-8 -*-

import sys
import unittest

class TestMyClass(unittest.TestCase):  # unittest.TestCase を要 継承

    # test_~()毎の実行前に呼ばれます
    def setUp(self):
        pass

    # test_~()毎の実行後に呼ばれます
    def tearDown(self):
        pass

    def test_a(self):
        func_name = sys._getframe().f_code.co_name

        # assert~()失敗時に停止させない為、with化
        tmp_comment = func_name
        with self.subTest(tmp_comment):
            self.assertEqual(True, [True,None],tmp_comment)
            self.assertEqual(False,[True,None],tmp_comment)


    def test_b(self):
        func_name = sys._getframe().f_code.co_name

        # assert~()失敗時に停止させない為、with化
        tmp_comment = func_name
        with self.subTest(tmp_comment):
            self.assertIn(1,[1,None],tmp_comment)
            self.assertIn(2,[1,None],tmp_comment)


        
# subTest()毎にtest数をcountする為のものです
class VerboseTestResult(unittest.TextTestResult):
    def addSubTest(self, test, subtest, outcome):
        super(VerboseTestResult, self).addSubTest(test, subtest, outcome)
        self.testsRun += 1


if __name__ == "__main__":
    runner = unittest.TextTestRunner(resultclass=VerboseTestResult,
                                     verbosity=2)
    unittest.main(testRunner=runner)
    #unittest.main(verbosity=2)

↑こう書くと、↓こう出力されます

..\python38\python.exe .\test_foo.py
test_a (__main__.TestMyClass) ... test_b (__main__.TestMyClass) ...
======================================================================
FAIL: test_a (__main__.TestMyClass) [test_a]
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".\foo.py", line 23, in test_a
    self.assertEqual(True, [True,None],tmp_comment)
AssertionError: True != [True, None] : test_a

======================================================================
FAIL: test_b (__main__.TestMyClass) [test_b]
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".\foo.py", line 34, in test_b
    self.assertIn(2,[1,None],tmp_comment)
AssertionError: 2 not found in [1, None] : test_b

----------------------------------------------------------------------
Ran 4 tests in 0.002s

FAILED (failures=2)

sprintf()がないjavascriptに、これを実装

sprintf:function(format){
    for (var i = 1; i < arguments.length; i++) {
        var reg = new RegExp('\\{' + (i - 1) + '\\}', 'g');

        format = format.replace(reg, arguments[i]);
    }
    return format;
}

↑こう定義し、↓こう使う

this.sprintf("{0} に、{1} が入ってます","A","1");

urllib.request for python による https で ERROR [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed

ERROR [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
Hostname mismatch, certificate is not valid for 'ないしょ.co.jp'.
(_ssl.c:1076) https://ないしょ.co.jp

ssl証明書の有効期限切れやホスト名誤りで上記のようなエラーとなるらしい。

これを無視し、処理を進めるには、以下のように 「ssl.create_default_https_context = ssl.create_unverified_context」を追加すれば、OK。

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import sys
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
import urllib.request


def main():
    req_url = "https://ないしょ.co.jp"
    req = urllib.request.Request(req_url)

    try:
        res = urllib.request.urlopen(req)
    except urllib.error.HTTPError as err:
        print("WARN",err.code, req_url,file=sys.stderr)
        return False
    except urllib.error.URLError as err:
        print("ERROR",err.reason, req_url,file=sys.stderr)
        return False

    print("DONE HTTP",err.code,req_url)


if __name__ == '__main__':
    main()