    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.fukurou.xml;
    017
    018import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
    019import org.opengion.fukurou.system.ThrowUtil;                                                   // 6.4.2.0 (2016/01/29)
    020import org.opengion.fukurou.util.FileUtil ;                                                     // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
    021import org.opengion.fukurou.system.Closer ;
    022import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.1.0.0 (2014/12/26) refactoring
    023
    024import java.sql.Connection;
    025import java.sql.SQLException;
    026
    027import java.io.Reader;
    028import java.io.BufferedReader;
    029import java.io.InputStreamReader;
    030import java.io.FileInputStream;
    031import java.io.InputStream;
    032import java.io.IOException;
    033import java.io.File;
    034import java.io.UnsupportedEncodingException;
    035import java.io.Writer;                                                          // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
    036
    037import java.util.Arrays;
    038import java.util.List;
    039import java.util.Map;
    040import java.util.ArrayList;
    041import java.util.Enumeration;
    042import java.util.jar.JarFile;
    043import java.util.jar.JarEntry;
    044import java.net.URL;
    045
    046/**
    047 * ORACLE XDK 形式のXMLファイルを読み取って、データベースに登録します。
    048 *
    049 * これは、Ver5の時は、org.opengion.hayabusa.common.InitFileLoader として
    050 * 使用されていたクラスを改造したものです。
    051 * InitFileLoader は、Ver6 では廃止されていますので、ご注意ください。
    052 *
    053 * 登録の実行有無の判断は、ファイルの更新時刻より判断します。(useTimeStamp=true の場合)
    054 * これは、読み取りファイルの更新時刻が、０でない場合、読み取りを行います。
    055 * 読み取りが完了した場合は、更新時刻を ０ に設定します。
    056 * 読み取るファイルは、クラスローダーのリソースや、指定のフォルダ以下のファイル、そして、
    057 * zip 圧縮されたファイルの中から、拡張子が xml で、UTF-8でエンコードされている
    058 * 必要があります。通常は、ファイル名がテーブル名と同一にしておく必要がありますが、
    059 * ROWSETのtable属性にテーブル名をセットしておくことも可能です。
    060 * ファイルの登録順は、原則、クラスローダーの検索順に、見つかった全てのファイルを
    061 * 登録します。データそのものは、INSERT のみ対応していますので、原則登録順は無視されます。
    062 * ただし、拡張XDK 形式で、EXEC_SQL タグを使用した場合は、登録順が影響する可能性があります。
    063 * 例：GE12.xml GE12 テーブルに登録するXMLファイル
    064 * 登録時に、既存のデータの破棄が必要な場合は、拡張XDK 形式のXMLファイルを
    065 * 作成してください。これは、EXEC_SQL タグに書き込んだSQL文を実行します。
    066 * 詳細は、{@link org.opengion.fukurou.xml.HybsXMLHandler HybsXMLHandler} クラスを参照してください。
    067 *
    068 *   &lt;ROWSET tableName="XX" &gt;
    069 *       &lt;EXEC_SQL&gt;                    最初に記載して、初期処理(データクリア等)を実行させる。
    070 *           delete from GEXX where YYYYY
    071 *       &lt;/EXEC_SQL&gt;
    072 *       &lt;ROW num="1"&gt;
    073 *           &lt;カラム1&gt;値1&lt;/カラム1&gt;
    074 *             ･･･
    075 *           &lt;カラムn&gt;値n&lt;/カラムn&gt;
    076 *       &lt;/ROW&gt;
    077 *        ･･･
    078 *       &lt;ROW num="n"&gt;
    079 *          ･･･
    080 *       &lt;/ROW&gt;
    081 *       &lt;EXEC_SQL&gt;                    最後に記載して、項目の設定(整合性登録)を行う。
    082 *           update GEXX set AA='XX' , BB='XX' where YYYYY
    083 *       &lt;/EXEC_SQL&gt;
    084 *   &lt;ROWSET&gt;
    085 *
    086 * @og.rev 4.0.0.0 (2004/12/31) 新規作成(org.opengion.hayabusa.common.InitFileLoader)
    087 * @og.rev 6.0.0.0 (2014/04/11) パッケージ、クラスファイル変更
    088 *
    089 * @version  6.0
    090 * @author   Kazuhiko Hasegawa
    091 * @since    JDK7.0,
    092 */
    093public final class XMLFileLoader {
    094        private final Connection connection ;
    095        private final boolean useTimeStamp ;
    096
    097        // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
    098        private Writer log = FileUtil.getLogWriter( "System.out" );
    099
    100        // 6.0.0.0 (2014/04/11) タイムスタンプのゼロクリア対象のファイルを管理するリスト
    101        private final List<File> fileList = new ArrayList<>();
    102
    103        // 6.0.0.0 (2014/04/11) setAfterMap メソッドの対応
    104        /** 6.4.3.1 (2016/02/12) 作成元のMapを、HashMap から ConcurrentHashMap に置き換え。  */
    105        private Map<String,String> afterMap     ;
    106
    107        // 6.0.0.0 (2014/04/11) 追加,更新,削除,実行 の各実行時のカウントの総数
    108        private final int[] crudCnt = new int[] { 0,0,0,0 };    // CRUD カウントですが、配列の順番は追加,更新,削除,実行
    109
    110        /** getCRUDCount() で返される カウント数の配列番号 {@value} */
    111        public static final int INS = 0;
    112        /** getCRUDCount() で返される カウント数の配列番号 {@value} */
    113        public static final int DEL = 1;
    114        /** getCRUDCount() で返される カウント数の配列番号 {@value} */
    115        public static final int UPD = 2;
    116        /** getCRUDCount() で返される カウント数の配列番号 {@value} */
    117        public static final int DDL = 3;
    118
    119        /**
    120         * コネクションを引数にする、コンストラクターです。
    121         * classPath="resource" で初期化された XMLFileLoader を作成します。
    122         * useTimeStamp 属性を true に設定すると、このファイルを読み取る都度
    123         * タイムスタンプを、クリアします。
    124         * また、タイムスタンプがクリアされたファイルは読み込みませんので、機能的に
    125         * 一度しか読み込まないという事になります。
    126         *
    127         * @param       conn            登録用コネクション
    128         * @param useTimeStamp  タイムスタンプの管理を行うかどうか[true:行う/false:行わない]
    129         */
    130        public XMLFileLoader( final Connection conn , final boolean useTimeStamp ) {
    131                connection                      = conn ;
    132                this.useTimeStamp       = useTimeStamp;
    133        }
    134
    135        /**
    136         * ログ出力を行う 内部ログ(Writer) を指定します。
    137         *
    138         * 内部ログ(Writer) の初期値は、null とします。
    139         * 内部ログ(Writer)が null の場合は、なにもしません。
    140         *
    141         * @og.rev 6.0.0.0 (2014/04/11) ログ関係を Writer で管理します。
    142         *
    143         * @param log Writerオブジェクト
    144         */
    145        public void setLogWriter( final Writer log ) {
    146                this.log = log ;
    147        }
    148
    149        /**
    150         * XMLファイルを読み取った後で指定するカラムと値のペア(マップ)情報をセットします。
    151         *
    152         * このカラムと値のペアのマップは、オブジェクト構築後に設定される為、
    153         * XMLファイルのキーの存在に関係なく、Mapのキーと値が使用されます。(Map優先)
    154         * null を設定した場合は、なにも処理されません。
    155         *
    156         * @og.rev 6.0.0.0 (2014/04/11) 新規追加
    157         *
    158         * @param map   後設定するカラムデータマップ
    159         */
    160        public void setAfterMap( final Map<String,String> map ) {
    161                afterMap = map;
    162        }
    163
    164        /**
    165         * XMLファイルを登録後の、追加,更新,削除,実行 のカウント配列を返します。
    166         *
    167         * 簡易的に処理したいために、配列に設定しています。
    168         * 順番に、追加,更新,削除,実行 のカウント値になります。
    169         *
    170         * @og.rev 6.0.0.0 (2014/04/11) 新規追加
    171         *
    172         * @return 追加,更新,削除,実行 のカウント配列
    173         * @og.rtnNotNull
    174         */
    175        public int[] getCRUDCount() {
    176                // 6.0.2.5 (2014/10/31) refactoring
    177                return crudCnt.clone();
    178        }
    179
    180        /**
    181         * 対象となるファイル群を ClassLoader の指定パスから、検索します。
    182         *
    183         * 対象ファイルは、指定フォルダに テーブル名.xml 形式で格納しておきます。
    184         * このフォルダのファイルをピックアップします。
    185         * useTimeStamp 属性を true に設定すると、このファイルを読み取る都度
    186         * タイムスタンプを、クリアします。
    187         * また、タイムスタンプがクリアされたファイルは読み込みませんので、機能的に
    188         * 一度しか読み込まないという事になります。
    189         *
    190         * @og.rev 6.0.0.0 (2014/04/11) 新規追加
    191         * @og.rev 6.4.0.4 (2015/12/26) Writer(ログ)のCloseは、ここでは行わない。
    192         * @og.rev 6.8.5.1 (2018/01/15) ファイル名は、##バージョン番号を変換しておく必要がある。
    193         *
    194         * @param path 対象となるファイル群を検索する、クラスパス
    195         */
    196        public void loadClassPathFiles( final String path ) {
    197                try {
    198                        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
    199                        final Enumeration<URL> enume = loader.getResources( path );
    200                        while( enume.hasMoreElements() ) {
    201                                final URL url = enume.nextElement();
    202                                // jar:file:/実ディレクトリ または、file:/実ディレクトリ
    203                                println( "      XMLFileLoader Scan:[ " + url + " ]" );                  // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
    204//                              final String dir = url.getFile();
    205                                final String dir = url.getFile().replaceAll( "%23%23","##" );   // 6.8.5.1 (2018/01/15)
    206
    207                                if( "jar".equals( url.getProtocol() ) ) {
    208                                        // dir = file:/C:/webapps/gf/WEB-INF/lib/resource2.jar!/resource 形式です。
    209                                        final String jar = dir.substring(dir.indexOf( ':' )+1,dir.lastIndexOf( '!' ));
    210                                        // jar = /C:/webapps/gf/WEB-INF/lib/resource2.jar 形式に切り出します。
    211
    212                                        // 5.6.6.1 (2013/07/12) jarファイルも、タイムスタンプ管理の対象
    213                                        loadJarFile( new File( jar ) );
    214                                }
    215                                else {
    216                                        // 5.3.6.0 (2011/06/01) 実フォルダの場合、フォルダ階層を下る処理を追加
    217                                        // dir = /C:/webapps/gf/WEB-INF/classes/resource/ 形式です。
    218                                        loadXMLDir( new File( dir ) );
    219                                }
    220                        }
    221                        Closer.commit( connection );
    222                }
    223                catch( final SQLException ex ) {
    224                        final String errMsg = "SQL実行時にエラーが発生しました。"
    225                                        + CR + ex.getMessage();
    226                        Closer.rollback( connection );
    227                        throw new OgRuntimeException( errMsg,ex );
    228                }
    229                catch( final IOException ex ) {
    230                        final String errMsg = "XMLファイル読み取り時にエラーが発生しました。"
    231                                        + CR + ex.getMessage();
    232                        throw new OgRuntimeException( errMsg,ex );
    233                }
    234                finally {
    235                        // 6.4.0.4 (2015/12/26) Writer(ログ)のCloseは、ここでは行わない。
    236                        setZeroTimeStamp();                             // 6.0.0.0 (2014/04/11) タイムスタンプの書き換えをメソッド化
    237                }
    238        }
    239
    240        /**
    241         * 対象となるファイル群を ファイル単体、フォルダ階層以下、ZIPファイル から、検索します。
    242         *
    243         * 対象ファイルは、テーブル名.xml 形式で格納しておきます。
    244         * この処理では、ファイル単体(*.xml)、フォルダ階層以下、ZIPファイル(*.jar , *.zip) は混在できません。
    245         * 最初に判定した形式で、個々の処理に振り分けています。
    246         *
    247         * @og.rev 6.0.0.0 (2014/04/11) 新規追加
    248         * @og.rev 6.4.0.4 (2015/12/26) Writer(ログ)のCloseは、ここでは行わない。
    249         *
    250         * @param       fileObj                 読取元のファイルオブジェクト
    251         * @see         #loadClassPathFiles( String )
    252         */
    253        public void loadXMLFiles( final File fileObj ) {
    254                try {
    255                        // nullでなく、ファイル/フォルダが存在することが前提
    256                        if( fileObj != null && fileObj.exists() ) {
    257                                println( "      XMLFileLoader Scan:[ " + fileObj + " ]" );      // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
    258                                loadXMLDir( fileObj );                  // ファイルかディレクトリ
    259                        }
    260                        Closer.commit( connection );
    261                }
    262                catch( final SQLException ex ) {
    263                        final String errMsg = "SQL実行時にエラーが発生しました。"
    264                                        + CR + ex.getMessage();
    265                        Closer.rollback( connection );
    266                        throw new OgRuntimeException( errMsg,ex );
    267                }
    268                catch( final IOException ex ) {
    269                        final String errMsg = "XMLファイル読み取り時にエラーが発生しました。"
    270                                        + CR + ex.getMessage();
    271                        throw new OgRuntimeException( errMsg,ex );
    272                }
    273                finally {
    274                        // 6.4.0.4 (2015/12/26) Writer(ログ)のCloseは、ここでは行わない。
    275                        setZeroTimeStamp();                             // 6.0.0.0 (2014/04/11) タイムスタンプの書き換えをメソッド化
    276                }
    277        }
    278
    279        /**
    280         * XMLフォルダ/ファイルを読み取り、データベースに追加(INSERT)するメソッドをコールします。
    281         *
    282         * ここでは、フォルダ階層を下るための再起処理を行っています。
    283         * XMLファイルは、ORACLE XDK拡張ファイルです。テーブル名を指定することで、
    284         * XMLファイルをデータベースに登録することが可能です。
    285         * ORACLE XDK拡張ファイルや、EXEC_SQLタグなどの詳細は、{@link org.opengion.fukurou.xml.HybsXMLSave}
    286         * を参照願います。
    287         *
    288         * @og.rev 5.3.6.0 (2011/06/01) 実フォルダの場合、フォルダ階層を下る処理を追加
    289         *
    290         * @param       jarObj                  読取元のJarファイルオブジェクト
    291         * @throws SQLException,IOException  データベースアクセスエラー、または、データ入出力エラー
    292         */
    293        private void loadJarFile( final File jarObj ) throws SQLException,IOException {
    294                if( ! useTimeStamp || jarObj.lastModified() > 0 ) {
    295                        JarFile jarFile = null;
    296                        try {
    297                                jarFile = new JarFile( jarObj );
    298                                final Enumeration<JarEntry> flEnum = jarFile.entries() ;                // 4.3.3.6 (2008/11/15) Generics警告対応
    299                                while( flEnum.hasMoreElements() ) {
    300                                        final JarEntry ent = flEnum.nextElement();                              // 4.3.3.6 (2008/11/15) Generics警告対応
    301                                        final String file = ent.getName();
    302                                        if( ! ent.isDirectory() && file.endsWith( ".xml" ) ) {
    303                                                // 5.6.6.1 (2013/07/12) jarファイルの中身のタイムスタンプは見ない。
    304                                                final String table = file.substring( file.lastIndexOf('/')+1,file.lastIndexOf('.') );
    305                                                InputStream stream = null;
    306                                                try {
    307                                                        stream = jarFile.getInputStream( ent ) ;
    308                                                        loadXML( stream,table,file );
    309                                                }
    310                                                finally {
    311                                                        Closer.ioClose( stream );
    312                                                }
    313                                        }
    314                                }
    315                                fileList.add( jarObj );                 // 5.6.6.1 (2013/07/12) jarファイルも、タイムスタンプ管理の対象
    316                        }
    317                        finally {
    318                                Closer.zipClose( jarFile );             // 5.5.2.6 (2012/05/25) findbugs対応
    319                        }
    320                }
    321        }
    322
    323        /**
    324         * XMLフォルダ/ファイルを読み取り、データベースに追加(INSERT)するメソッドをコールします。
    325         *
    326         * ここでは、フォルダ階層を下るための再起処理を行っています。
    327         * XMLファイルは、ORACLE XDK拡張ファイルです。テーブル名を指定することで、
    328         * XMLファイルをデータベースに登録することが可能です。
    329         * ORACLE XDK拡張ファイルや、EXEC_SQLタグなどの詳細は、{@link org.opengion.fukurou.xml.HybsXMLSave}
    330         * を参照願います。
    331         *
    332         * @og.rev 5.3.6.0 (2011/06/01) 実フォルダの場合、フォルダ階層を下る処理を追加
    333         *
    334         * @param       fileObj                 読取元のファイルオブジェクト
    335         * @throws SQLException,IOException  データベースアクセスエラー、または、データ入出力エラー
    336         */
    337        private void loadXMLDir( final File fileObj ) throws SQLException,IOException {
    338                if( fileObj.isDirectory() ) {
    339                        final File[] list = fileObj.listFiles();
    340                        // 6.3.9.0 (2015/11/06) null になっている可能性がある(findbugs)
    341                        if( list != null ) {
    342                                Arrays.sort( list );
    343                                for( final File file : list ) {
    344                                        loadXMLDir( file );
    345                                }
    346                        }
    347                }
    348                else if( ! useTimeStamp || fileObj.lastModified() > 0 ) {
    349                        final String name = fileObj.getName() ;
    350                        if( name.endsWith( ".xml" ) ) {
    351                                final String table = name.substring( name.lastIndexOf('/')+1,name.lastIndexOf('.') );
    352                                InputStream stream = null;
    353                                try {
    354                                        println( "        " + fileObj );                // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
    355                                        stream = new FileInputStream( fileObj ) ;
    356                                        loadXML( stream,table,fileObj.getPath() );
    357                                        fileList.add( fileObj );                                // 正常に処理が終われば、リストに追加します。
    358                                }
    359                                finally {
    360                                        Closer.ioClose( stream );
    361                                }
    362                        }
    363                        else if( name.endsWith( ".zip" ) || name.endsWith( ".jar" ) ) {
    364                                loadJarFile( fileObj );
    365                        }
    366                }
    367        }
    368
    369        /**
    370         * XMLファイルを読み取り、データベースに追加(INSERT)します。
    371         *
    372         * XMLファイルは、ORACLE XDK拡張ファイルです。テーブル名を指定することで、
    373         * XMLファイルをデータベースに登録することが可能です。
    374         * ORACLE XDK拡張ファイルや、EXEC_SQLタグなどの詳細は、{@link org.opengion.fukurou.xml.HybsXMLSave}
    375         * を参照願います。
    376         *
    377         * @og.rev 5.6.6.1 (2013/07/12) 更新カウント数も取得します。
    378         * @og.rev 5.6.7.0 (2013/07/27) HybsXMLSave の DDL（データ定義言語：Data Definition Language）の処理件数追加
    379         * @og.rev 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行しない。
    380         * @og.rev 6.2.0.0 (2015/02/27) try ～ finally 構文
    381         * @og.rev 7.3.2.0 (2021/03/19) isExecErr でfalseを指定した場合に、エラー内容の文字列を取り出します。
    382         *
    383         * @param       stream  XMLファイルを読み取るInputStream
    384         * @param       table   テーブル名(ROWSETタグのtable属性が未設定時に使用)
    385         * @param       file    ログ出力用のファイル名
    386         * @see org.opengion.fukurou.xml.HybsXMLSave
    387         */
    388        private void loadXML( final InputStream stream, final String table, final String file )
    389                                                                        throws SQLException,UnsupportedEncodingException {
    390                // 6.2.0.0 (2015/02/27) try ～ finally 構文
    391                Reader reader = null;
    392                try {
    393                        // InputStream より、XMLファイルを読み取り、table に追加(INSERT)します。
    394                        reader = new BufferedReader( new InputStreamReader( stream,"UTF-8" ) );
    395                        final HybsXMLSave save = new HybsXMLSave( connection,table );
    396                        save.onExecErrException( false );               // 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行しない。
    397                        save.setAfterMap( afterMap );                   // 6.0.0.0 (2014/04/11) 新規追加
    398                        save.insertXML( reader );
    399
    400                        final int insCnt = save.getInsertCount();
    401                        final int delCnt = save.getDeleteCount();
    402                        final int updCnt = save.getUpdateCount();               // 5.6.6.1 (2013/07/12) 更新カウント数も取得
    403                        final int ddlCnt = save.getDDLCount();                  // 5.6.7.0 (2013/07/27) DDL処理件数追加
    404
    405                        crudCnt[INS] += insCnt ;
    406                        crudCnt[DEL] += delCnt ;
    407                        crudCnt[UPD] += updCnt ;
    408                        crudCnt[DDL] += ddlCnt ;
    409
    410                        final String tableName = save.getTableName() ;
    411
    412                        // 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
    413                        println( "          File=[" + file + "] TABLE=[" + tableName + "] DEL=["+ delCnt +"] INS=[" + insCnt + "] UPD=[" + updCnt + "] DDL=[" + ddlCnt + "]" );
    414
    415                        // 7.3.2.0 (2021/03/19) isExecErr でfalseを指定した場合に、エラー内容の文字列を取り出します。
    416                        final String errMsg = save.getErrorMessage() ;
    417                        if( !errMsg.isEmpty() ) {
    418                                println( errMsg );
    419                        }
    420                }
    421                finally {
    422                        Closer.ioClose( reader );
    423                }
    424        }
    425
    426        /**
    427         * 指定のリストのファイルのタイムスタンプをゼロに設定します。
    428         * useTimeStamp=true の時に、XMLファイルのロードに成功したファイルの
    429         * タイムスタンプをゼロに設定することで、２回目の処理が避けられます。
    430         *
    431         * @og.rev 6.0.0.0 (2014/04/11) 新規追加
    432         */
    433        private void setZeroTimeStamp() {
    434                if( useTimeStamp ) {
    435                        for( final File file : fileList ) {
    436                                if( !file.setLastModified( 0L ) ) {
    437                                        final String errMsg = "タイムスタンプの書き換えに失敗しました。"
    438                                                                        + "file=" + file ;
    439                                        System.err.println( errMsg );
    440                                }
    441                        }
    442                }
    443        }
    444
    445        /**
    446         * 登録されている ログ(Writer) に、メッセージを書き込みます。
    447         * メッセージの最後に改行を挿入します。
    448         *
    449         * 内部ログ(Writer)が null の場合は、なにもしません。
    450         * 内部ログ(Writer) の初期値は、標準出力(System.out) から作成された Writerです。
    451         *
    452         * @og.rev 6.0.0.0 (2014/04/11) ログ関係を Writer で管理
    453         * @og.rev 6.4.2.0 (2016/01/29) ex.printStackTrace() を、ThrowUtil#ogStackTrace(Throwable) に置き換え。
    454         *
    455         * @param       msg 書き出すメッセージ
    456         */
    457        private void println( final String msg ) {
    458                if( log != null ) {
    459                        try {
    460                                log.write( msg );
    461                                log.write( CR );
    462                        }
    463                        catch( final IOException ex ) {
    464                                // ファイルが書き込めなかった場合
    465                                final String errMsg = msg + " が、書き込めませんでした。"
    466                                                                        + ex.getMessage() ;
    467                                System.err.println( errMsg );
    468                                System.err.println( ThrowUtil.ogStackTrace( ex ) );                             // 6.4.2.0 (2016/01/29)
    469                        }
    470                }
    471        }
    472}