本家「@IT」にはない内容をエンジニアライフで技術紹介するコラム。広く議論する場になることを目指します。

第058回_一般実体テーブル作成処理の実装2

»

MakingGeTableVisitorの実装を紹介する前にVisitorの設計を明確化するために少し工夫をします。

同一構文・異なる処理に対する工夫

今回処理を記載する部分の構文は次の範囲です。
 GEDecl ::= Name S GeEntityDef SYMBOL ('>')
 GeEntityDef ::= GeInternalDef | GeExternalDef
 GeInternalDef ::= EntityValue S?
 GeExternalDef ::= ExternalID NDataDecl?
 NDataDecl ::= S ('NDATA' S Name S?)?

ここで、
 Name
 EntityValue
 ExternalID
は他の構文にも出てくる内容です。
# NameはGEDeclにもNDataDeclにも出てくるので判り易いです。
これらは、同じnodeのまま扱うと出てくる位置による処理の違いを表現できません。

ExternalID
-背景
例えばExternalIDは

出現箇所意味と処理内容
doctypedecl外部サブセットのURIを表現し、doctypedecl内で内部サブセットを読み込んだ後に外部サブセットを読み込む
EntityDef外部一般実体のURIを表現し、URIを一般実体テーブルに保存する
PEDef外部パラメータ実体のURIを表現し、URIをパラメータ実体テーブルに保存する
NotationDeclアプリケーション側が利用する何等かのURIを表現し、記法テーブルに保存する

のように4か所に出てきて、それぞれのExternalIDにて処理が異なります。

-解決案1 それぞれのExternalIDと子ノードの非終端と処理が必要なnodeを別のnodeとして表現する
例えば、
 GeExternalDef ::= GeExternalID NDataDecl?
 GeExternalID ::= GeSystemLiteralOfExternalId | GePubidLiteralOfExternalId
 GeSystemLiteralOfExternalId ::= 'SYSTEM' S GeSystemLiteral
 GePubidLiteralOfExternalId ::= 'PUBLIC' S GePubidLiteral S GeSystemLiteral
 GeSystemLiteral ::= LITERALトークン
 GePubidLiteral ::= LITERAL (PubidChar*)

 PEDef ::= EntityValue PeExternalID
 PeExternalID ::= PeSystemLiteralOfExternalId | PePubidLiteralOfExternalId
 PeSystemLiteralOfExternalId ::= 'SYSTEM' S PeSystemLiteral
 PePubidLiteralOfExternalId ::= 'PUBLIC' S PePubidLiteral S PeSystemLiteral
 PeSystemLiteral ::= LITERALトークン
 PePubidLiteral ::= LITERAL (PubidChar*) とすれば、GeExternalDefとPEDefにあるExternalIDは同一の構文ではなくなるので異なる処理を書くことができます。ただし、この方針を採用するとEbnfXXXNodeクラスは同一の処理でも同一構文の共有ができなくなります。

案1はnodeクラスの肥大化が懸念となります。自動生成の場合は案1を採用して問題ありません。自動生成でない場合はVisitorで対処した案2以降を考える必要が出てきます。

-解決案2 お互いのVisitorの処理範囲が被らないようにする
 doctypedeclを処理するVisitor
 EntityDefを処理するVisitor
 PEDefを処理するVisitor
 NotationDeclを処理するVisitor
のお互いの処理範囲が被らなければ各Visitorが異なる処理を書いても問題ありません。構文階層で言えば、doctypedeclの中のマークアップ宣言である、GEDeclとPEDeclとNotationDeclは、それぞれ異なるVisitorで表現するだけで処理範囲は被らなくなります。doctypedeclはGEDeclとPEDeclとNotationDeclを含む範囲の構文ですが、各構文を異なるVisitorとして呼び出すようにすれば同じnodeであったとしても正しく機能します。

案2の調整が上手くいかない場合は案1に戻りますが、ExternalIDの処理の場合は案2で解決できます。ただし、構文設計者の意図を考えると、ExternalIDのデータを取得する処理はほぼ同じなので、この部分を共通化する方法も考えましょう。

-解決案2改 構文の共通部分でデータ生成が同じなら1つのVisitorで実装する
(フィールドにPubidLiteralとSystemLiteralを持つ)ExernalIDというデータクラスを作ることを考える場合、
doctypedeclを処理するVisitor
EntityDefを処理するVisitor
PEDefを処理するVisitor
NotationDeclを処理するVisitor
では全く同じ処理ですから、このためのMakingExternalIDVisitorを用意すると共通化できます。
ただし、それぞれの異なる処理部分を
 doctypedeclを処理するVisitor
 EntityDefを処理するVisitor
 PEDefを処理するVisitor
 NotationDeclを処理するVisitor
側で実装するためのラッパーノードが必要になります。例えば、
 GeExternalDef ::= GeExternalID NDataDecl?
 GeExternalID (継承 ExternalID)
とすることで、
MakingGeTableVisitor#visit(EbnfGeExternalIDNode n)
{
    MakingExternalIDVisitor v = new  MakingExternalIDVisitor();
    
    //自動的にvisit(EbnfExtenralIDNode n)として処理する
    n.accept(v);
    
    m_result = v.getResult();
    
    if(m_result)
    {
       m_externalID = v.getExtenralID();
       
       //個別の処理:一般実体テーブルに保存する
    }
}
ExternalIDは案2改で実装することができます。

Name
Nameの考え方もExternalIDのように考えることができます、
ただし、
単一のNodeであること、
Nameは完全なデータ構造だけで処理を持たないこと
を考慮すると少しだけ事情が変わります。 まず、案1のように実装することはノードの増加を招く以外には問題がありません。
次に、案2のように実装しようとしても単一ノードのため意味がありません。
しかし、Nameの親構文(Nameを含む構文)がたった1つのNameだけをデータとして扱う場合は案2を採用する意味が
出てきます。Nameが出てくる構文の内、処理が必要になる構文は
 [28] doctypedecl ::= '<!DOCTYPE' S Name (S ExternalID)? S? ('[' intSubset ']' S?)? '>'
 [40] STag ::= '<' Name (S Attribute)* S? '>'
 [41] Attribute ::= Name Eq AttValue
 [44] EmptyElemTag ::= '<' Name (S Attribute)* S? '/>'
 [48] cp ::= (Name | choice | seq) ('?' | '*' | '+')?
 [51] Mixed ::= '(' S? '#PCDATA' (S? '|' S? Name)* S? ')*' | '(' S? '#PCDATA' S? ')'
 [52] AttlistDecl ::= '<!ATTLIST' S Name AttDef* S? '>'
 [53] AttDef ::= S Name S AttType S DefaultDecl
 [58] NotationType ::= 'NOTATION' S '(' S? Name (S? '|' S? Name)* S? ')'
 [68] EntityRef ::= '&' Name ';'
 [69] PEReference ::= '%' Name ';'
 [71] GEDecl ::= '<!ENTITY' S Name S EntityDef S? '>'
 [72] PEDecl ::= ''<!ENTITY' S '%' S Name S PEDef S? ''>'
 [76] NDataDecl ::= S 'NDATA' S Name
 [82] NotationDecl ::= '<!NOTATION' S Name S (ExternalID | PublicID) S? '>'
で、その内で単独のNameだけをデータに持つ構文は、
 [68] EntityRef ::= '&' Name ';'
 [69] PEReference ::= '%' Name ';'
 [76] NDataDecl ::= S 'NDATA' S Name
となります。ただし、NDataDecl自体も特別な処理がなく、実質、
 GeExternalDef ::= ExternalID (S 'NDATA' S Name)?
であることを考慮すると、GeExternalDefはName以外にもExternalIDを持ち、案2改の適用範囲外となります。
結局、対象となる構文は、
 [68] EntityRef ::= '&' Name ';'
 [69] PEReference ::= '%' Name ';'
となります。つまり、GEDeclの子ノードについて考えた場合、
 GEDecl ::= GeName S GeEntityDef SYMBOL ('>')
 GeName(Nameを継承)
 NDataDecl ::= S ('NDATA' S NDataName S?)?
 NDataName(Nameを継承)
になりますが、
 EntityRef ::= '&' Name ';'
 PEReference ::= '%' Name ';'
についてはNameのままにしておきます。

EntityValue
EntityValueはGEDeclとPEDeclで出現し処理も同じですので、Visitorを共通化する意味があります。ただし、他の包含関係があるので少し検討が必要です。
 EntityValue ::= SYMBOL ('"') (NORMAL_TOKEN_B2 | PEReference | Reference)* SYMBOL ('"')
        | SYMBOL ("'") (NORMAL_TOKEN_B1 | PEReference | Reference)* SYMBOL ("'")
 PEReference ::= SYMBOL ('%') Name SYMBOL (';')
 Reference ::= EntityRef | CharRef
 EntityRef ::= SYMBOL ('&') Name SYMBOL (';')
 CharRef ::= SYMBOL ('&#') WORD ([0-9]+) SYMBOL (';') | SYMBOL ('&#x') WORD ([0-9a-fA-F]+) ) SYMBOL (';')
の内、CharRefは出現位置が複数あっても処理が一緒です。一方PEReferenceとReferenceは、出現位置が複数あって且、処理が異なります。

-CharRefを処理する共通Visitorによる修正
CharRefVisitorを使うために、EvCharRefを追加して
 Reference ::= EntityRef | EvCharRef
 EvCharRef(CharRefを継承)
とします。

-PEReference/Referenceを処理するための修正
 EvPEReference(PEReferenceを継承)

 EvReference :: EvEntityRef | EvCharRef
 EvEntityRef(EvEntityRefを継承)
を追加して、
 EntityValue ::= SYMBOL ('"') (NORMAL_TOKEN_B2 | EvPEReference | EvReference)* SYMBOL ('"')
        | SYMBOL ("'") (NORMAL_TOKEN_B1 | EvPEReference | EvReference)* SYMBOL ("'")
に修正します。

CharRefVisitorの実装

工夫の内容を元にCharRefVisitorを実装します。

実装は次のようにできます。
public class CharRefVisitor extends SyntaxParserVisitor
{
    //文字参照で示すコードポイントから参照する文字を得る変換器
    private static CharRefConvertor m_charRefConvertor
                             = CharRefConvertor.getInstance();
    private String m_decimalCharRef;
    private String m_hexCharRef;
    private String m_value;
    
    public CharRefVisitor()
    {
        m_decimalCharRef = null;
        m_hexCharRef = null;
        m_value = "";
    }
    
    public String getValue()
    {
        return m_value;
    }

    @Override
    public void visit(EbnfHexCharRefNode n)
    {
        super.visit(n);
        
        if (m_result)
        {
            //巡回用データに文字列を追加する
            m_value +=m_hexCharRef;
        }
        else
        {
            //何もしない
        }
    }
    
    @Override
    //Charef(&#x unicode ;) のunicode部分を文字に変換する
    public void visit(EbnfHexCharRefCoreNode n)
    {
        Token token = m_tokenManager.nextToken();
        m_result = token.match(n);
        
        if(m_result)
        {
            //処理に成功したので巡回用データに登録する
            String str = token.getString();
            m_hexCharRef = m_charRefConvertor.convertOneHexUnicodeToString(str);
        }
        else
        {
            //何もしない
        }
    }

    @Override
    public void visit(EbnfDecimalCharRefNode n)
    {
        super.visit(n);
        
        if (m_result)
        {
            //巡回用データに文字列を追加する
            m_value+=m_decimalCharRef;
        }
        else
        {
            //何もしない
        }
    }
    
    @Override
    //Charef(&# unicode ;) のunicode部分を文字に変換する
    public void visit(EbnfDecimalCharRefCoreNode n)
    {
        Token token = m_tokenManager.nextToken();
        m_result = token.match(n);
        
        if(m_result)
        {
            //処理に成功したので巡回用データに登録する
            String str = token.getString();
            m_decimalCharRef = m_charRefConvertor.convertOneUnicodeToString(str);
        }
        else
        {
            //何もしない
        }
    }
}

CharRefConvertorの実装は次のようになります。
public class CharRefConvertor
{
    private static CharRefConvertor m_instance = new CharRefConvertor();

    private final IntegerRangeKeyMap<Boolean> m_charMap = new IntegerRangeKeyMap<Boolean>();
    
    
    private CharRefConvertor()
    {
        m_charMap.put('\u0001', '\uD7FF', true);
        m_charMap.put('\uE000', '\uFFFD', true);
        //サロゲートペアは未対応
        m_charMap.setOutOfRange(false);
    }
    
    public static CharRefConvertor getInstance()
    {
        return m_instance;
    }
    
    //Charef(&# unicode;)のunicode部分を受け取って文字に戻す
    public String convertOneUnicodeToString(String unicode)
    {
        int[] codePoints = new int[1];
        codePoints[0] = new Integer(unicode).intValue();
                
        String retStr = new String(codePoints, 0, 1);
        
        //Charの範囲内か確認する
        boolean isChar = this.isChar(retStr.charAt(0));
        
        //Charの範囲内の場合
        if (isChar)
        {
            //何もしない
        }
        //Charの範囲内でない場合
        else
        {
            //TODO
            //  整形式制約違反
            
            //空文字にする
            retStr = "";
        }
        
        return retStr;
    }    

    //Charef(&#x unicode;)のunicode部分を受け取って文字に戻す
    public String convertOneHexUnicodeToString(String unicode)
    {
        int[] codePoints = new int[1];
        codePoints[0] = Integer.parseInt(unicode,16);
                
        String retStr = new String(codePoints, 0, 1);

        //Charの範囲内か確認する
        boolean isChar = this.isChar(retStr.charAt(0));
        
        //Charの範囲内の場合
        if (isChar)
        {
            //何もしない
        }
        //Charの範囲内でない場合
        else
        {
            //TODO
            //  整形式制約違反

            //空文字にする
            retStr = "";
        }
        
        return retStr;
    }
    
    private boolean isChar(char txt)
    {
        return m_charMap.get(txt);
    }
}
TODO:整形式制約違反の個所は、エラー/ログ出力について考えるまでそのままにしておきます。
長くなったので次回に続きます。
Comment(0)

コメント

コメントを投稿する