HyperSQLQueryObject.java

  1. /*
  2.  * GovWay - A customizable API Gateway
  3.  * https://govway.org
  4.  *
  5.  * Copyright (c) 2005-2025 Link.it srl (https://link.it).
  6.  *
  7.  * This program is free software: you can redistribute it and/or modify
  8.  * it under the terms of the GNU General Public License version 3, as published by
  9.  * the Free Software Foundation.
  10.  *
  11.  * This program is distributed in the hope that it will be useful,
  12.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14.  * GNU General Public License for more details.
  15.  *
  16.  * You should have received a copy of the GNU General Public License
  17.  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  18.  *
  19.  */

  20. package org.openspcoop2.utils.sql;

  21. import java.text.SimpleDateFormat;
  22. import java.util.Date;
  23. import java.util.Iterator;

  24. import org.openspcoop2.utils.TipiDatabase;
  25. import org.openspcoop2.utils.date.DateUtils;

  26. /**
  27.  * Implementazione per HyperSQL
  28.  *
  29.  * @author Poli Andrea (apoli@link.it)
  30.  * @author $Author$
  31.  * @version $Rev$, $Date$
  32.  */
  33. public class HyperSQLQueryObject extends SQLQueryObjectCore {

  34.     public HyperSQLQueryObject(TipiDatabase tipoDatabase) {
  35.         super(tipoDatabase);
  36.     }

  37.     @Override
  38.     protected String getPrefixCastValue(CastColumnType type, int length) {
  39.         switch (type) {
  40.         case INT:
  41.         case LONG:
  42.         case STRING:
  43.         case TIMESTAMP:
  44.             return "CAST( ";
  45.         case NONE:
  46.             return "";
  47.         }
  48.         return "";
  49.     }
  50.     @Override
  51.     protected String getSuffixCastValue(CastColumnType type, int length) {
  52.         StringBuilder sb = new StringBuilder();
  53.         sb.append(" AS ");
  54.         switch (type) {
  55.         case INT:
  56.             sb.append("INT");
  57.             break;
  58.         case LONG:
  59.             sb.append("BIGINT");
  60.             break;
  61.         case STRING:
  62.             sb.append("VARCHAR("+length+")");
  63.             break;
  64.         case TIMESTAMP:
  65.             sb.append("TIMESTAMP");
  66.             break;
  67.         case NONE:
  68.             return "";
  69.         }
  70.         sb.append(")");
  71.         return sb.toString();
  72.     }
  73.    
  74.    
  75.    
  76.     /**
  77.      * Ritorna una costante  di tipo 'timestamp'
  78.      *
  79.      * @param date Costante
  80.      */
  81.     @Override
  82.     public String getSelectTimestampConstantField(Date date) throws SQLQueryObjectException{
  83.         SimpleDateFormat sqlDateformat = DateUtils.getDefaultDateTimeFormatter("yyyy-MM-dd HH:mm:ss.SSS");
  84.         return "TIMESTAMP('"+sqlDateformat.format(date)+"')";
  85.     }
  86.    
  87.    
  88.    
  89.    
  90.     @Override
  91.     public String getUnixTimestampConversion(String column){
  92.         /**return "((datediff('ss',CAST('1970-01-01 00:00:00' AS TIMESTAMP),"+column+")-3600)*1000)";*/
  93.         return "( "+
  94.                 "("+
  95.                 "(UNIX_TIMESTAMP("+column+")*1000) - "+
  96.                 "((EXTRACT(HOUR FROM TIMEZONE()))*60*60*1000)" +
  97.                 ") + "+
  98.                 "(CAST(TO_CHAR ("+column+",'FF') AS INTEGER))" +
  99.                 ")"
  100.                 ;
  101.     }
  102.    
  103.     @Override
  104.     public String getDiffUnixTimestamp(String columnMax,String columnMin){
  105.         return "( "+getUnixTimestampConversion(columnMax)+" - "+getUnixTimestampConversion(columnMin)+" )";
  106.     }
  107.    
  108.    
  109.    
  110.     @Override
  111.     public ISQLQueryObject addSelectAvgTimestampField(String field, String alias)
  112.             throws SQLQueryObjectException {
  113.         if(field==null)
  114.             throw new SQLQueryObjectException(SQLQueryObjectCore.FIELD_DEVE_ESSERE_DIVERSO_NULL);
  115.         // Trasformo in UNIX_TIMESTAMP
  116.         String fieldSQL = "avg("+this.getUnixTimestampConversion(field)+")";
  117.         if(alias != null){
  118.             /** fieldSQL = fieldSQL + " as "+alias;*/
  119.             fieldSQL = fieldSQL +this.getDefaultAliasFieldKeyword()+alias;
  120.         }
  121.         this.engineAddSelectField(null,fieldSQL,null,false,true);
  122.         this.fieldNames.add(alias);
  123.         return this;
  124.     }

  125.     @Override
  126.     public ISQLQueryObject addSelectMaxTimestampField(String field, String alias)
  127.             throws SQLQueryObjectException {
  128.         if(field==null)
  129.             throw new SQLQueryObjectException(SQLQueryObjectCore.FIELD_DEVE_ESSERE_DIVERSO_NULL);
  130.         // Trasformo in UNIX_TIMESTAMP
  131.         String fieldSQL = "max("+this.getUnixTimestampConversion(field)+")";
  132.         if(alias != null){
  133.             /**fieldSQL = fieldSQL + " as "+alias;*/
  134.             fieldSQL = fieldSQL +this.getDefaultAliasFieldKeyword()+alias;
  135.         }
  136.         this.engineAddSelectField(null,fieldSQL,null,false,true);
  137.         this.fieldNames.add(alias);
  138.         return this;
  139.     }

  140.     @Override
  141.     public ISQLQueryObject addSelectMinTimestampField(String field, String alias)
  142.             throws SQLQueryObjectException {
  143.         if(field==null)
  144.             throw new SQLQueryObjectException(SQLQueryObjectCore.FIELD_DEVE_ESSERE_DIVERSO_NULL);
  145.         // Trasformo in UNIX_TIMESTAMP
  146.         String fieldSQL = "min("+this.getUnixTimestampConversion(field)+")";
  147.         if(alias != null){
  148.             /**fieldSQL = fieldSQL + " as "+alias;*/
  149.             fieldSQL = fieldSQL +this.getDefaultAliasFieldKeyword()+alias;
  150.         }
  151.         this.engineAddSelectField(null,fieldSQL,null,false,true);
  152.         this.fieldNames.add(alias);
  153.         return this;
  154.     }

  155.     @Override
  156.     public ISQLQueryObject addSelectSumTimestampField(String field, String alias)
  157.             throws SQLQueryObjectException {
  158.         if(field==null)
  159.             throw new SQLQueryObjectException(SQLQueryObjectCore.FIELD_DEVE_ESSERE_DIVERSO_NULL);
  160.         // Trasformo in UNIX_TIMESTAMP
  161.         String fieldSQL = "sum("+this.getUnixTimestampConversion(field)+")";
  162.         if(alias != null){
  163.             /**fieldSQL = fieldSQL + " as "+alias;*/
  164.             fieldSQL = fieldSQL +this.getDefaultAliasFieldKeyword()+alias;
  165.         }
  166.         this.engineAddSelectField(null,fieldSQL,null,false,true);
  167.         this.fieldNames.add(alias);
  168.         return this;
  169.     }
  170.    
  171.    
  172.    
  173.    
  174.    
  175.    
  176.     @Override
  177.     public ISQLQueryObject addFromTable(ISQLQueryObject subSelect)
  178.             throws SQLQueryObjectException {
  179.         StringBuilder bf = new StringBuilder();
  180.         bf.append(" ( ");
  181.         bf.append(subSelect.createSQLQuery());
  182.         bf.append(" ) ");
  183.         this.addFromTable(bf.toString());
  184.         return this;
  185.     }
  186.    
  187.    
  188.    
  189.    
  190.     @Override
  191.     protected EscapeSQLConfiguration getEscapeSQLConfiguration(){
  192.        
  193.         EscapeSQLConfiguration config = new EscapeSQLConfiguration();
  194.         config.addCharacter('_');
  195.         config.addCharacter('%');
  196.         config.addCharacter('\\');
  197.         config.setUseEscapeClausole(true);
  198.         config.setEscape('\\');
  199.        
  200.         // special
  201.         config.addCharacterWithOtherEscapeChar('\'','\'');
  202.        
  203.         return config;
  204.     }
  205.    
  206.    
  207.    
  208.    
  209.    
  210.    
  211.     @Override
  212.     public String getExtractDayFormatFromTimestampFieldPrefix(DayFormatEnum dayFormat) throws SQLQueryObjectException {
  213.         if(dayFormat==null) {
  214.             throw new SQLQueryObjectException("dayFormat undefined");
  215.         }
  216.         if(DayFormatEnum.DAY_OF_WEEK.equals(dayFormat)) {
  217.             return "DAYOFWEEK(";
  218.         }
  219.         else {
  220.             return super.getExtractDayFormatFromTimestampFieldPrefix(dayFormat);
  221.         }
  222.     }
  223.     @Override
  224.     public String getExtractDayFormatFromTimestampFieldSuffix(DayFormatEnum dayFormat) throws SQLQueryObjectException {
  225.         if(DayFormatEnum.DAY_OF_WEEK.equals(dayFormat)) {
  226.             return ")";
  227.         }
  228.         else {
  229.             return super.getExtractDayFormatFromTimestampFieldSuffix(dayFormat);
  230.         }
  231.     }
  232.    
  233.    
  234.    
  235.    
  236.    
  237.    
  238.     @Override
  239.     public String createSQLQuery() throws SQLQueryObjectException{
  240.         return this.createSQLQuery(false);
  241.     }
  242.     private String createSQLQuery(boolean union) throws SQLQueryObjectException{
  243.        
  244.         this.precheckBuildQuery();
  245.        
  246.         StringBuilder bf = new StringBuilder();
  247.        
  248.         bf.append("SELECT ");
  249.        
  250.         // Limit (senza offset)
  251.         if(this.offset<0 &&
  252.             (this.limit>0)
  253.             ){
  254.             bf.append(" TOP ");
  255.             bf.append(this.limit);
  256.             bf.append(" ");
  257.         }
  258.        
  259.         // forzatura di indici
  260.         Iterator<String> itForceIndex = this.forceIndexTableNames.iterator();
  261.         while(itForceIndex.hasNext()){
  262.             bf.append(" "+itForceIndex.next()+" ");
  263.         }
  264.        
  265.         if(this.isSelectDistinct())
  266.             bf.append(" DISTINCT ");
  267.        
  268.         // select field
  269.         if(this.fields.isEmpty()){
  270.             bf.append("*");
  271.         }else{
  272.             Iterator<String> it = this.fields.iterator();
  273.             boolean first = true;
  274.             while(it.hasNext()){
  275.                 if(!first)
  276.                     bf.append(",");
  277.                 else
  278.                     first = false;
  279.                 bf.append(it.next());
  280.             }
  281.         }
  282.        
  283.         bf.append(getSQL(false,false,false,union));
  284.        
  285.         /**if( this.offset>=0 || this.limit>=0)
  286.         //  System.out.println("SQL ["+bf.toString()+"]");*/
  287.        
  288.         return bf.toString();
  289.     }

  290.    
  291.    
  292.    
  293.     @Override
  294.     public String createSQLDeleteEngine() throws SQLQueryObjectException {
  295.         StringBuilder bf = new StringBuilder();
  296.                
  297.         bf.append("DELETE ");
  298.        
  299.         bf.append(getSQL(true,false,false,false));
  300.         return bf.toString();
  301.     }
  302.    
  303.    
  304.    
  305.    

  306.    
  307.     /**
  308.      * @return SQL
  309.      * @throws SQLQueryObjectException
  310.      */
  311.     private String getSQL(boolean delete,boolean update,boolean conditions,boolean union) throws SQLQueryObjectException {
  312.         StringBuilder bf = new StringBuilder();

  313.         if(this.selectForUpdate){
  314.             this.checkSelectForUpdate(update, delete, union);
  315.         }
  316.        
  317.         if(!update && !conditions){
  318.             // From
  319.             bf.append(SQLQueryObjectCore.FROM_SEPARATOR);
  320.            
  321.             // Table dove effettuare la ricerca 'FromTable'
  322.             if(this.tables.isEmpty()){
  323.                 throw new SQLQueryObjectException(SQLQueryObjectCore.TABELLA_RICERCA_FROM_NON_DEFINITA);
  324.             }else{
  325.                 if(delete && this.tables.size()>2)
  326.                     throw new SQLQueryObjectException("Non e' possibile effettuare una delete con piu' di una tabella alla volta");
  327.                 Iterator<String> it = this.tables.iterator();
  328.                 boolean first = true;
  329.                 while(it.hasNext()){
  330.                     if(!first)
  331.                         bf.append(",");
  332.                     else
  333.                         first = false;
  334.                     bf.append(it.next());
  335.                 }
  336.             }
  337.         }
  338.        
  339.         // Condizioni di Where
  340.         if(!this.conditions.isEmpty()){
  341.            
  342.             if(!conditions)
  343.                 bf.append(SQLQueryObjectCore.WHERE_SEPARATOR);
  344.            
  345.             if(this.notBeforeConditions){
  346.                 bf.append("NOT (");
  347.             }
  348.            
  349.             for(int i=0; i<this.conditions.size(); i++){
  350.                 if(i>0){
  351.                     if(this.andLogicOperator){
  352.                         bf.append(SQLQueryObjectCore.AND_SEPARATOR);
  353.                     }else{
  354.                         bf.append(SQLQueryObjectCore.OR_SEPARATOR);
  355.                     }
  356.                 }
  357.                 bf.append(this.conditions.get(i));
  358.             }
  359.            
  360.             if(this.notBeforeConditions){
  361.                 bf.append(")");
  362.             }
  363.         }
  364.        
  365.         // Condizione GroupBy
  366.         if((!this.getGroupByConditions().isEmpty()) && (!delete) && (!update) && (!conditions)){
  367.             bf.append(SQLQueryObjectCore.GROUP_BY_SEPARATOR);
  368.             Iterator<String> it = this.getGroupByConditions().iterator();
  369.             boolean first = true;
  370.             while(it.hasNext()){
  371.                 if(!first)
  372.                     bf.append(",");
  373.                 else
  374.                     first = false;
  375.                 bf.append(it.next());
  376.             }
  377.         }
  378.        
  379.         // Condizione OrderBy
  380. /**     if(union==false){ La condizione di OrderBy DEVE essere generata. In SQLServer e MySQL viene generata durante la condizione di LIMIT/OFFSET*/
  381.         // NOTA: L'order by insieme al LIMIT e OFFSET, all'interno di sotto-select come in questo caso della union, funziona solo con HSQL 2.x
  382.             if((!this.orderBy.isEmpty()) && (!delete) && (!update) && (!conditions) ){
  383.                 bf.append(SQLQueryObjectCore.ORDER_BY_SEPARATOR);
  384.                 Iterator<String> it = this.orderBy.iterator();
  385.                 boolean first = true;
  386.                 while(it.hasNext()){
  387.                     String column = it.next();
  388.                     if(!first)
  389.                         bf.append(",");
  390.                     else
  391.                         first = false;
  392.                     bf.append(column);
  393.                     boolean sortTypeAsc = this.sortTypeAsc;
  394.                     if(this.orderBySortType.containsKey(column)){
  395.                         sortTypeAsc = this.orderBySortType.get(column);
  396.                     }
  397.                     if(sortTypeAsc){
  398.                         bf.append(SQLQueryObjectCore.ASC_SEPARATOR);
  399.                     }else{
  400.                         bf.append(SQLQueryObjectCore.DESC_SEPARATOR);
  401.                     }
  402.                 }
  403.             }
  404. /**     }*/
  405.        
  406.         // Limit e Offset
  407.         /**if(this.limit>0 || this.offset>0){*/
  408.         // Rilascio vincolo di order by in caso di limit impostato.
  409.         // Il vincolo rimane per l'offset, per gestire le select annidate di qualche implementazioni come Oracle,SQLServer ...
  410.         if(this.offset>=0 &&
  411.             (this.orderBy.isEmpty())
  412.             ){
  413.             throw new SQLQueryObjectException(SQLQueryObjectCore.CONDIZIONI_ORDER_BY_RICHESTE);
  414.         }
  415.        
  416.         // Limit (con offset)
  417.         if(this.offset>=0 &&
  418.             ((this.limit>0) && (!delete) && (!update) && (!conditions))
  419.             ){
  420.             bf.append(SQLQueryObjectCore.LIMIT_SEPARATOR);
  421.             bf.append(this.limit);
  422.         }
  423.        
  424.         // Offset
  425.         if((this.offset>=0) && (!delete) && (!update) && (!conditions)){
  426.             bf.append(" OFFSET ");
  427.             bf.append(this.offset);
  428.         }
  429.            
  430.         // ForUpdate
  431.         if(!conditions &&
  432.             (this.selectForUpdate)
  433.             ){
  434.             bf.append(" FOR UPDATE ");
  435.         }
  436.        
  437.         return bf.toString();
  438.     }
  439.    
  440.    
  441.    
  442.     @Override
  443.     public String createSQLUnion(boolean unionAll,
  444.             ISQLQueryObject... sqlQueryObject) throws SQLQueryObjectException {
  445.        
  446.         // Controllo parametro su cui effettuare la UNION
  447.         this.checkUnionField(false,sqlQueryObject);
  448.        
  449.         if(this.selectForUpdate){
  450.             this.checkSelectForUpdate(false, false, true);
  451.         }
  452.        
  453.         StringBuilder bf = new StringBuilder();
  454.        
  455.         bf.append("SELECT ");
  456.        
  457.         // Non ha senso, la union fa gia la distinct, a meno di usare la unionAll ma in quel caso non si vuole la distinct
  458.         /** if(this.isSelectDistinct())
  459.         //  bf.append(" DISTINCT ");*/
  460.        
  461.         // Limit (senza offset)
  462.         if(this.offset<0 &&
  463.             (this.limit>0)
  464.             ){
  465.             bf.append(" TOP ");
  466.             bf.append(this.limit);
  467.             bf.append(" ");
  468.         }
  469.        
  470.         // forzatura di indici
  471.         Iterator<String> itForceIndex = this.forceIndexTableNames.iterator();
  472.         while(itForceIndex.hasNext()){
  473.             bf.append(" "+itForceIndex.next()+" ");
  474.         }
  475.                
  476.         // select field
  477.         if(this.fields.isEmpty()){
  478.             bf.append("*");
  479.         }else{
  480.             Iterator<String> it = this.fields.iterator();
  481.             boolean first = true;
  482.             while(it.hasNext()){
  483.                 if(!first)
  484.                     bf.append(",");
  485.                 else
  486.                     first = false;
  487.                 bf.append(it.next());
  488.             }
  489.         }
  490.        
  491.         bf.append(SQLQueryObjectCore.FROM_SEPARATOR_APERTURA);
  492.        
  493.         for(int i=0; i<sqlQueryObject.length; i++){
  494.            
  495.             if(((HyperSQLQueryObject)sqlQueryObject[i]).selectForUpdate){
  496.                 try{
  497.                     ((HyperSQLQueryObject)sqlQueryObject[i]).checkSelectForUpdate(false, false, true);
  498.                 }catch(Exception e){
  499.                     throw new SQLQueryObjectException("Parametro SqlQueryObject["+i+"] non valido: "+e.getMessage());
  500.                 }
  501.             }
  502.            
  503.             if(i>0){
  504.                 bf.append(" UNION ");
  505.                 if(unionAll){
  506.                     bf.append(" ALL ");
  507.                 }
  508.             }
  509.            
  510.             bf.append("( ");
  511.            
  512.             bf.append(((HyperSQLQueryObject)sqlQueryObject[i]).createSQLQuery(true));
  513.            
  514.             bf.append(") ");
  515.         }
  516.        
  517.         bf.append(SQLQueryObjectCore.AS_SUBQUERY_SUFFIX+getSerial()+" ");
  518.        
  519.         // Condizione GroupBy
  520.         if((!this.getGroupByConditions().isEmpty()) ){
  521.             bf.append(SQLQueryObjectCore.GROUP_BY_SEPARATOR);
  522.             Iterator<String> it = this.getGroupByConditions().iterator();
  523.             boolean first = true;
  524.             while(it.hasNext()){
  525.                 if(!first)
  526.                     bf.append(",");
  527.                 else
  528.                     first = false;
  529.                 bf.append(it.next());
  530.             }
  531.         }
  532.        
  533.         // Condizione OrderBy
  534.         if(!this.orderBy.isEmpty()){
  535.             bf.append(SQLQueryObjectCore.ORDER_BY_SEPARATOR);
  536.             Iterator<String> it = this.orderBy.iterator();
  537.             boolean first = true;
  538.             while(it.hasNext()){
  539.                 String column = it.next();
  540.                 if(!first)
  541.                     bf.append(",");
  542.                 else
  543.                     first = false;
  544.                 bf.append(column);
  545.                 boolean sortTypeAsc = this.sortTypeAsc;
  546.                 if(this.orderBySortType.containsKey(column)){
  547.                     sortTypeAsc = this.orderBySortType.get(column);
  548.                 }
  549.                 if(sortTypeAsc){
  550.                     bf.append(SQLQueryObjectCore.ASC_SEPARATOR);
  551.                 }else{
  552.                     bf.append(SQLQueryObjectCore.DESC_SEPARATOR);
  553.                 }
  554.             }
  555.         }
  556.        
  557.         // Limit e Offset
  558.         /**if(this.limit>0 || this.offset>0){*/
  559.         // Rilascio vincolo di order by in caso di limit impostato.
  560.         // Il vincolo rimane per l'offset, per gestire le select annidate di qualche implementazioni come Oracle,SQLServer ...
  561.         if(this.offset>=0 &&
  562.             (this.orderBy.isEmpty())
  563.             ){
  564.             throw new SQLQueryObjectException(SQLQueryObjectCore.CONDIZIONI_ORDER_BY_RICHESTE);
  565.         }
  566.        
  567.         // Limit (con offset)
  568.         if(this.offset>=0 &&
  569.             (this.limit>0)
  570.             ){
  571.             bf.append(SQLQueryObjectCore.LIMIT_SEPARATOR);
  572.             bf.append(this.limit);
  573.         }
  574.        
  575.         // Offset
  576.         if(this.offset>=0){
  577.             bf.append(" OFFSET ");
  578.             bf.append(this.offset);
  579.         }
  580.        
  581.         return bf.toString();
  582.        
  583.     }

  584.     @Override
  585.     public String createSQLUnionCount(boolean unionAll, String aliasCount,
  586.             ISQLQueryObject... sqlQueryObject) throws SQLQueryObjectException {
  587.         // Controllo parametro su cui effettuare la UNION
  588.         this.checkUnionField(true,sqlQueryObject);
  589.        
  590.         if(aliasCount==null){
  591.             throw new SQLQueryObjectException("Alias per il count non definito");
  592.         }
  593.        
  594.         StringBuilder bf = new StringBuilder();
  595.        
  596.         bf.append("SELECT count(*) "+this.getDefaultAliasFieldKeyword()+" ");
  597.         bf.append(aliasCount);
  598.         bf.append(SQLQueryObjectCore.FROM_SEPARATOR_APERTURA);
  599.        
  600.         bf.append( this.createSQLUnion(unionAll, sqlQueryObject) );
  601.        
  602.         bf.append(SQLQueryObjectCore.AS_SUBQUERY_SUFFIX+getSerial()+" ");
  603.            
  604.         return bf.toString();
  605.     }
  606.    
  607.    
  608.    

  609.     @Override
  610.     public String createSQLUpdateEngine() throws SQLQueryObjectException {

  611.         StringBuilder bf = new StringBuilder();
  612.         bf.append("UPDATE ");
  613.         bf.append(this.updateTable);
  614.         bf.append(" SET ");
  615.         for(int i=0; i<this.updateFieldsName.size(); i++){
  616.             if(i>0)
  617.                 bf.append(" , ");
  618.             bf.append(this.updateFieldsName.get(i));
  619.             bf.append(" = ");
  620.             bf.append(this.updateFieldsValue.get(i));
  621.         }
  622.         bf.append(getSQL(false,true,false,false));
  623.         return bf.toString();
  624.     }

  625.    

  626.    
  627.    
  628.     /* ---------------- WHERE CONDITIONS ------------------ */
  629.    
  630.     @Override
  631.     public String createSQLConditionsEngine() throws SQLQueryObjectException {
  632.        
  633.         StringBuilder bf = new StringBuilder();
  634.         bf.append(getSQL(false,false,true,false));
  635.         return bf.toString();
  636.     }

  637.    

  638.    
  639.    
  640.    
  641.    
  642.    

  643. }