Statistics
| Revision:

svn-gvsig-desktop / trunk / org.gvsig.desktop / org.gvsig.desktop.plugin / org.gvsig.h2spatial / org.gvsig.h2spatial.h2gis132 / org.gvsig.h2spatial.h2gis132.provider / src / main / java / org / gvsig / fmap / dal / store / h2 / H2SpatialHelper.java @ 45499

History | View | Annotate | Download (22.7 KB)

1
/* gvSIG. Geographic Information System of the Valencian Government
2
 *
3
 * Copyright (C) 2007-2020 Infrastructures and Transports Department
4
 * of the Valencian Government (CIT)
5
 *
6
 * This program is free software; you can redistribute it and/or
7
 * modify it under the terms of the GNU General Public License
8
 * as published by the Free Software Foundation; either version 3
9
 * of the License, or (at your option) any later version.
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, write to the Free Software
18
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19
 * MA  02110-1301, USA.
20
 *
21
 */
22
package org.gvsig.fmap.dal.store.h2;
23

    
24
import java.io.File;
25
import java.sql.Connection;
26
import java.sql.SQLException;
27
import java.text.MessageFormat;
28
import org.apache.commons.dbcp.BasicDataSource;
29
import org.apache.commons.io.FilenameUtils;
30
import org.apache.commons.lang3.StringUtils;
31
import org.gvsig.expressionevaluator.GeometryExpressionBuilderHelper.GeometrySupportType;
32
import static org.gvsig.fmap.dal.DatabaseWorkspaceManager.FIELD_CONFIGURATION_NAME;
33
import static org.gvsig.fmap.dal.DatabaseWorkspaceManager.FIELD_CONFIGURATION_VALUE;
34
import static org.gvsig.fmap.dal.DatabaseWorkspaceManager.FIELD_RESOURCES_NAME;
35
import static org.gvsig.fmap.dal.DatabaseWorkspaceManager.FIELD_RESOURCES_RESOURCE;
36
import static org.gvsig.fmap.dal.DatabaseWorkspaceManager.TABLE_CONFIGURATION_NAME;
37
import static org.gvsig.fmap.dal.DatabaseWorkspaceManager.TABLE_RESOURCES_NAME;
38
import org.gvsig.fmap.dal.exception.InitializeException;
39
import org.gvsig.fmap.dal.resource.exception.AccessResourceException;
40
import org.gvsig.fmap.dal.spi.DataServerExplorerProviderServices;
41
import static org.gvsig.fmap.dal.store.h2.H2SpatialHelper.LOGGER;
42
import org.gvsig.fmap.dal.store.h2.operations.H2SpatialOperationsFactory;
43
import org.gvsig.fmap.dal.store.jdbc.JDBCConnectionParameters;
44
import org.gvsig.fmap.dal.store.jdbc.JDBCNewStoreParameters;
45
import org.gvsig.fmap.dal.store.jdbc.JDBCServerExplorerParameters;
46
import org.gvsig.fmap.dal.store.jdbc.JDBCStoreParameters;
47
import org.gvsig.fmap.dal.store.jdbc.exception.JDBCDriverClassNotFoundException;
48
import org.gvsig.fmap.dal.store.jdbc2.JDBCServerExplorer;
49
import org.gvsig.fmap.dal.store.jdbc2.JDBCUtils;
50
import org.gvsig.fmap.dal.store.jdbc2.OperationsFactory;
51
import org.gvsig.fmap.dal.store.jdbc2.spi.ConnectionProvider;
52
import org.gvsig.fmap.dal.store.jdbc2.spi.JDBCHelperBase;
53
import org.gvsig.fmap.dal.store.jdbc2.spi.JDBCSQLBuilderBase;
54
import org.gvsig.fmap.dal.store.jdbc2.spi.SRSSolverBase;
55
import org.gvsig.fmap.dal.store.jdbc2.spi.SRSSolverDumb;
56
import org.h2.tools.Server;
57
import org.h2gis.functions.factory.H2GISFunctions;
58
import org.h2gis.functions.system.H2GISversion;
59
import org.slf4j.Logger;
60
import org.slf4j.LoggerFactory;
61

    
62

    
63
@SuppressWarnings("UseSpecificCatch")
64
public class H2SpatialHelper extends JDBCHelperBase {
65

    
66
    static final Logger LOGGER = LoggerFactory.getLogger(H2SpatialHelper.class);
67

    
68
    public static final String H2SPATIAL_JDBC_DRIVER = "org.h2.Driver";
69
    
70
    public static File getLocalFile(H2SpatialConnectionParameters params) {
71
        String host = params.getHost();
72
        if( !StringUtils.isEmpty(host) ) {
73
          host = host.toLowerCase().trim();
74
          if( !(host.equals("localhost") || host.equals("127.0.0.1")) ) {
75
            return null;
76
          }
77
        }
78
        File f = params.getFile();
79
        if( f == null ) {
80
          return null;
81
        }
82
        String pathname = f.getAbsolutePath().replace("\\","/");
83
        if( !pathname.endsWith(".mv.db")  ) {
84
          pathname += ".mv.db";
85
        }      
86
        
87
        return new File(pathname);
88
    }
89
    
90
    public static String getConnectionURL(H2SpatialConnectionParameters params) {
91
        String connectionURL;
92
        String dbfilename = params.getFile().getAbsolutePath().replace("\\","/");
93
        if( dbfilename!=null && dbfilename.endsWith(".mv.db") ) {
94
            dbfilename = dbfilename.substring(0, dbfilename.length()-6);
95
        }
96
        StringBuilder commonParameters = new StringBuilder();
97
        commonParameters.append(";MODE=PostgreSQL");
98
        commonParameters.append(";SCHEMA=PUBLIC");
99
        commonParameters.append(";ALLOW_LITERALS=ALL");
100
        if( StringUtils.isEmpty(params.getHost()) ) {
101
            // Asumimos que es una conexion directa sobre el filesystem
102
            if( StringUtils.equalsIgnoreCase(FilenameUtils.getExtension(params.getFile().getName()),"zip") ) {
103
                connectionURL =  MessageFormat.format(
104
                    "jdbc:h2:zip:{0}!/{1}"+commonParameters.toString(),
105
                    dbfilename,
106
                    params.getDBName()
107
                );
108
            } else {
109
                connectionURL =  MessageFormat.format(
110
                    "jdbc:h2:file:{0}"+commonParameters.toString(),
111
                    dbfilename
112
                );
113
            }
114
        } else if( params.getPort() == null ) {
115
            connectionURL =  MessageFormat.format(
116
                "jdbc:h2:tcp://{0}/{1}"+commonParameters.toString(),
117
                params.getHost(),
118
                dbfilename
119
            );            
120
        } else {
121
            connectionURL =  MessageFormat.format("jdbc:h2:tcp://{0}:{1,number,#######}/{2}"+commonParameters.toString(),
122
                params.getHost(),
123
                (int) params.getPort(),
124
                dbfilename
125
            );
126
        }
127
        LOGGER.debug("connectionURL: {}", connectionURL);
128
        return connectionURL;
129
    }
130

    
131
    public static class ConnectionProviderImpl implements ConnectionProvider {
132

    
133
        private static boolean needRegisterDriver = true;
134

    
135
        private BasicDataSource dataSource = null;
136

    
137
        private final H2SpatialConnectionParameters connectionParameters;
138
        
139
        private static Server server = null;
140
        private static boolean startServer = true;
141

    
142
        public ConnectionProviderImpl(H2SpatialConnectionParameters connectionParameters) {
143
            this.connectionParameters = connectionParameters;
144
        }
145

    
146
        @Override
147
        public String getStatus() {
148
            StringBuilder builder = new StringBuilder();
149
            builder.append("Pool: ");
150
            builder.append(JDBCUtils.getHexId(dataSource));
151
            builder.append(" Actives: ");
152
            builder.append(dataSource.getNumActive());
153
            builder.append("/");
154
            builder.append(dataSource.getMaxActive());
155
            builder.append(" idle: ");
156
            builder.append(dataSource.getNumIdle());
157
            builder.append("/");
158
            builder.append(dataSource.getMinIdle());
159
            builder.append(":");
160
            builder.append(dataSource.getMaxIdle());
161
            return builder.toString();
162
        }
163
        
164
        @SuppressWarnings("ConvertToTryWithResources")
165
        public void shutdown() {
166
            LOGGER.info("Shutdown H2 connection.");
167
            try {
168
                Connection conn = this.getConnection();
169
                conn.createStatement().execute("SHUTDOWN");
170
                conn.close();
171
            } catch (Throwable th) {
172
                LOGGER.warn("Problems shutdown the database.", th);
173
            }
174
            try {
175
                if( dataSource!=null ) {
176
                    LOGGER.info("Clossing connection pool.");
177
                    LOGGER.info(this.getStatus());
178
                    dataSource.close();
179
                    LOGGER.info("Connection pool closed.");
180
                    LOGGER.info(this.getStatus());
181
                }
182
            } catch (Throwable th) {
183
                LOGGER.warn("Problems closing connections pool.", th);
184
            }
185
        }
186
        
187
        public static void stopServer() {
188
            if (server == null) {
189
                LOGGER.info("The H2 server is already stopped.");
190
            } else {
191
                LOGGER.info("Stopping the H2 server.");
192
                LOGGER.info("  port  :" + server.getPort());
193
                LOGGER.info("  URL   :" + server.getURL());
194
                LOGGER.info("  shutdown server...");
195
                try {
196
                    server.shutdown();
197
                } catch (Throwable th) {
198
                    LOGGER.warn("Problems shutdown the H2 server.", th);
199
                }
200
                LOGGER.info("  Stoping server...");
201
                try {
202
                    server.stop();
203
                } catch (Throwable th) {
204
                    LOGGER.warn("Problems stopping the H2 server.", th);
205
                }
206
                LOGGER.info("  status:" + server.getStatus());
207
                server = null;
208
                LOGGER.info("H2 Server stopped");
209
            }
210
            startServer = true;
211
        }
212
        
213
        private void startServer() {
214
        
215
            if( startServer && server == null ) {
216
                String port = "9123";
217
                try {
218
                    Server theServer;
219
                    if( this.connectionParameters.getServerPort()>0 ) {
220
                        port = String.valueOf(this.connectionParameters.getServerPort());
221
                    }
222
                    if( this.connectionParameters.getServerAllowOthers() ) {
223
                        theServer = Server.createTcpServer("-tcpPort", port, "-ifExists", "-tcpAllowOthers");
224
                    } else {
225
                        theServer = Server.createTcpServer("-tcpPort", port, "-ifExists");
226
                    }
227
                    theServer.start();
228
                    server = theServer;
229
                    LOGGER.info("H2 Server started" );
230
                    LOGGER.info("  Engine version : h2 "+ org.h2.engine.Constants.getFullVersion()+", h2gis "+H2GISversion.geth2gisVersion());
231
                    LOGGER.info("  Connection url : jdbc:h2:"+server.getURL()+"/ABSOLUTE_DATABASE_PATH;MODE=PostgreSQL;SCHEMA=PUBLIC;ALLOW_LITERALS=ALL");
232
//                    LOGGER.info("  port  :"+ server.getPort());
233
//                    LOGGER.info("  URL   :"+ server.getURL());
234
                    LOGGER.info("  status:"+ server.getStatus());
235
                    Runtime.getRuntime().addShutdownHook(new Thread() {
236
                        @Override
237
                        public void run() {
238
                            stopServer();
239
                        }
240
                    });
241
                } catch (SQLException ex) {
242
                    LOGGER.warn("H2 Server not started",ex);
243
                }
244
                // Tanto si consigue lanzar el server como si no, no lo vuelve a intentar
245
                startServer = false;
246
            }
247

    
248
        }
249

    
250
        @Override
251
        public String toString() {
252
            StringBuilder builder = new StringBuilder();
253
            builder.append(" url=").append(connectionParameters.getUrl());
254
            builder.append(" driver name=").append(connectionParameters.getJDBCDriverClassName());
255
            builder.append(" user=").append(connectionParameters.getUser());
256
            return builder.toString();
257
        }
258
        
259
        @Override
260
        public synchronized Connection getConnection() throws SQLException {
261
            File f = H2SpatialHelper.getLocalFile(connectionParameters);
262
            boolean newdb = !f.exists();
263
            
264
            if (this.dataSource == null) {
265
                this.dataSource = this.createDataSource();               
266
            }
267
            Connection conn;
268
            try {
269
                conn = this.dataSource.getConnection();
270
            } catch(Throwable th) {
271
                LOGGER.warn("Can't create connection to '"+this.dataSource.getUrl()+"'. "+this.getStatus());
272
                LOGGER.warn("Can't create connection to '"+this.dataSource.getUrl()+"'.",th);
273
                throw th;
274
            }
275
            try {
276
                conn.createStatement().execute("SELECT TOP 1 SRID FROM SPATIAL_REF_SYS");
277
            } catch(SQLException ex) {
278
                H2GISFunctions.load(conn);
279
            }
280
            try {
281
                conn.createStatement().execute("CREATE SCHEMA IF NOT EXISTS PUBLIC;SET SCHEMA PUBLIC");
282
            } catch(SQLException ex) {
283
                LOGGER.trace("Can't create schema public.",ex);
284
                // Ignore this error.
285
            }
286
            
287
            if( newdb ) {
288
                    String[] sqls = new String[] {
289
                        "CREATE CACHED TABLE PUBLIC.\""+TABLE_RESOURCES_NAME+"\"(\""+FIELD_RESOURCES_NAME+"\" VARCHAR(150) NOT NULL, \""+FIELD_RESOURCES_RESOURCE+"\" BLOB DEFAULT NULL)",
290
                        "ALTER TABLE PUBLIC.\""+TABLE_RESOURCES_NAME+"\" ADD CONSTRAINT PUBLIC.CONSTRAINT_E PRIMARY KEY(\""+FIELD_RESOURCES_NAME+"\")",
291
                        "CREATE CACHED TABLE PUBLIC.\""+TABLE_CONFIGURATION_NAME+"\"(\""+FIELD_CONFIGURATION_NAME+"\" VARCHAR(200) NOT NULL, \""+FIELD_CONFIGURATION_VALUE+"\" VARCHAR(200) DEFAULT NULL)",
292
                        "ALTER TABLE PUBLIC.\""+TABLE_CONFIGURATION_NAME+"\" ADD CONSTRAINT PUBLIC.CONSTRAINT_2 PRIMARY KEY(\""+FIELD_CONFIGURATION_NAME+"\")"
293
                    };
294
                    for (String sql : sqls) {
295
                        try {
296
                            conn.createStatement().execute(sql);
297
                        } catch(SQLException ex) {
298
                            LOGGER.debug("Can't configure gvsig tables.",ex);
299
                            LOGGER.warn("Can't configure gvsig tables. "+sql);
300
                            // Ignore this error.
301
                        }
302
                    }
303
            }
304
            return conn;
305
        }
306
        
307
        private BasicDataSource createDataSource() throws SQLException {
308
            if (!this.isRegistered()) {
309
                this.registerDriver();
310
            }
311
            startServer();
312
            H2SpatialConnectionParameters params = connectionParameters;
313

    
314
            BasicDataSource ds = new BasicDataSource();
315
            ds.setDriverClassName(params.getJDBCDriverClassName());
316
            if( !StringUtils.isEmpty(params.getUser()) ) {
317
                ds.setUsername(params.getUser());
318
            }
319
            if( !StringUtils.isEmpty(params.getPassword()) ) {
320
                ds.setPassword(params.getPassword());
321
            }
322
            ds.setUrl(params.getUrl());
323

    
324
            ds.setMaxWait(60L * 1000);
325
            
326
            //
327
            // Ajustamos el pool para que las conexiones se cierren a los
328
            // 10 segundos, asi tratamos de que al salir de gvSIG no queden
329
            // conexiones abiertas con la BBDD y pueda quedar corrupta esta.
330
            // Hay que tener en cuenta que es una BBDD embebida, y mientras
331
            // hayan conexiones abiertas pueden quedar cosas por bajar a disco.
332
            //
333
            int sidle = this.connectionParameters.getMaxSecondsIdle();
334
            if( sidle < 0 ) {
335
                ds.setTimeBetweenEvictionRunsMillis(-1);
336
                ds.setMinEvictableIdleTimeMillis(30*1000);
337
            } else {
338
                // Revisamos las conexiones inactivas cada 10 segundos
339
                ds.setTimeBetweenEvictionRunsMillis(sidle*1000);
340
                // Eliminadmos las conexiones que lleven inactivas mas de 10 segundos.
341
                ds.setMinEvictableIdleTimeMillis(sidle*1000);
342
            }
343
            
344
            // Ajustamos el numero minimo de conexiones a 0 para permitir
345
            // que se lleguen a cerrar todas las conexiones del pool.
346
            ds.setMinIdle(0);
347
            // dejaremos el MaxIdle a 20, no parece importante. .
348
            ds.setMaxIdle(20);
349
            
350
            return ds;
351
        }
352

    
353
        private boolean isRegistered() {
354
            return needRegisterDriver;
355
        }
356

    
357
        @Override
358
        public void registerDriver() throws SQLException {
359
            String className = this.connectionParameters.getJDBCDriverClassName();
360
            if (className == null) {
361
                return;
362
            }
363
            try {
364
                Class theClass = Class.forName(className);
365
                if (theClass == null) {
366
                    throw new JDBCDriverClassNotFoundException(H2SpatialLibrary.NAME, className);
367
                }
368
            } catch (Exception e) {
369
                throw new SQLException("Can't register JDBC driver '" + className + "'.", e);
370
            }
371
            needRegisterDriver = false;
372
        }
373

    
374
    }
375

    
376
    private ConnectionProvider connectionProvider = null;
377
 
378
    /**
379
     * Constructor for use only for testing purposes.
380
     * 
381
     * @param connectionParameters
382
     * @param connectionProvider
383
     */
384
    public H2SpatialHelper(JDBCConnectionParameters connectionParameters, ConnectionProvider connectionProvider) { 
385
        super(connectionParameters);
386
        this.srssolver = new SRSSolverDumb(this);
387
        this.connectionProvider = connectionProvider;
388
    }
389
  
390
    public H2SpatialHelper(JDBCConnectionParameters connectionParameters) {
391
        super(connectionParameters);
392
        this.srssolver = new SRSSolverBase(this);
393
    }
394

    
395
    
396
    public void  shutdown() {
397
        try {
398
            if( this.connectionProvider!=null ) {
399
                ((ConnectionProviderImpl) this.connectionProvider).shutdown();
400
                this.connectionProvider = null;
401
            }
402
            ConnectionProviderImpl.stopServer();
403
        } catch (Throwable ex) {
404
            LOGGER.warn("Problems shutdown H2", ex);
405
        }
406
    }
407

    
408
    private void logConnectionStatus(String msg, Connection conn) {
409
        ConnectionProvider cp = this.getConnectionProvider();
410
        StringBuilder builder = new StringBuilder();
411
        builder.append(msg);
412
        if( conn == null ) {
413
            builder.append(": connection null");
414
        } else {
415
            Boolean closed = null;
416
            try {
417
                closed = conn.isClosed();
418
            } catch(Throwable th) {
419
            }
420
            builder.append(": connection ");
421
            builder.append(JDBCUtils.getConnId(conn));
422
            if( closed ) {
423
                builder.append(" (c)");
424
            }
425
            builder.append(" ");
426
        }
427
        builder.append(cp.getStatus());
428
        LOGGER.info(builder.toString());
429
    }
430
        
431
    private ConnectionProvider getConnectionProvider() {
432
        if (this.connectionProvider == null) {
433
          H2SpatialConnectionParameters connectionParameters = this.getConnectionParameters();
434
          if( connectionParameters==null ) {
435
            return null; // Testing mode?
436
          }
437
          this.connectionProvider = new ConnectionProviderImpl(connectionParameters);
438
        }
439
        return this.connectionProvider;
440
    }
441
    
442
    @Override
443
    public synchronized Connection  getConnection() throws AccessResourceException {
444
        try {
445
            if (this.connectionProvider == null) {
446
              H2SpatialConnectionParameters connectionParameters = this.getConnectionParameters();
447
              if( connectionParameters==null ) {
448
                return null; // Testing mode?
449
              }
450
              this.connectionProvider = new ConnectionProviderImpl(connectionParameters);
451
            }
452
            Connection connection = this.connectionProvider.getConnection();
453
            if( LOGGER.isDebugEnabled() ) {
454
                LOGGER.debug("["+JDBCUtils.getConnId(connection)+"] getConnection "+connectionProvider.getStatus()+" "+ connectionProvider.toString());
455
            }
456
            return connection;
457
        } catch (SQLException ex) {
458
            throw new AccessResourceException(H2SpatialLibrary.NAME, ex);
459
        }
460
    }
461

    
462
    @Override
463
    public void closeConnection(Connection connection) {
464
      if( connection!=null ) { // In test ???
465
        LOGGER.debug("["+JDBCUtils.getConnId(connection)+"] closeConnection "+ this.connectionProvider.getStatus());
466
      }
467
      super.closeConnection(connection);
468
    }
469
    
470
    @Override
471
    public H2SpatialConnectionParameters getConnectionParameters() {
472
        return (H2SpatialConnectionParameters) super.getConnectionParameters();
473
    }
474
    
475
    @Override
476
    public String getConnectionURL() {
477
        return getConnectionURL(this.getConnectionParameters());
478
    }
479

    
480
    @Override
481
    protected String getResourceType() {
482
        return H2SpatialLibrary.NAME;
483
    }
484

    
485
    @Override
486
    public String getProviderName() {
487
        return H2SpatialLibrary.NAME;
488
    }
489

    
490
    @Override
491
    public JDBCSQLBuilderBase createSQLBuilder() {
492
        return new H2SpatialSQLBuilder(this);
493
    }
494
    
495
    @Override
496
    public OperationsFactory getOperations() {
497
        if (this.operationsFactory == null) {
498
            this.operationsFactory = new H2SpatialOperationsFactory(this);
499
        }
500
        return operationsFactory;
501
    }
502

    
503
    @Override
504
    public GeometrySupportType getGeometrySupportType() {
505
        return GeometrySupportType.WKB;
506
    }
507

    
508
    @Override
509
    public boolean hasSpatialFunctions() {
510
        return true;
511
    }
512

    
513
    @Override
514
    public boolean canWriteGeometry(int geometryType, int geometrySubtype) {
515
        return true;
516
    }
517

    
518
    @Override
519
    public String getQuoteForIdentifiers() {
520
        return "\"";
521
    }
522

    
523
    @Override
524
    public boolean allowAutomaticValues() {
525
        return true;
526
    }
527

    
528
    @Override
529
    public boolean supportOffsetInSelect() {
530
        return true;
531
    }
532

    
533
    @Override
534
    public String getQuoteForStrings() {
535
        return "'";
536
    }
537

    
538
    @Override
539
    public String getSourceId(JDBCStoreParameters parameters) {
540
        H2SpatialStoreParameters h2params = (H2SpatialStoreParameters) parameters;
541
        StringBuilder builder = new StringBuilder();
542
        builder.append(h2params.getTable());
543
        builder.append("(");
544
        if( StringUtils.isNotBlank(h2params.getHost()) ) {
545
            builder.append(h2params.getHost());
546
        }
547
        if( h2params.getPort()>0 ) {
548
            builder.append(",");
549
            builder.append(h2params.getPort());
550
        }
551
        File f = h2params.getFile();       
552
        if( f != null ) {
553
            builder.append(",");
554
            builder.append(h2params.getFile().getAbsolutePath());
555
        }
556
        builder.append(")");
557
        return builder.toString();
558
    }
559

    
560
    @Override
561
    public JDBCNewStoreParameters createNewStoreParameters() {
562
        return new H2SpatialNewStoreParameters();
563
    }
564

    
565
    @Override
566
    public JDBCStoreParameters createOpenStoreParameters() {
567
        return new H2SpatialStoreParameters();
568
    }
569

    
570
    @Override
571
    public JDBCServerExplorerParameters createServerExplorerParameters() {
572
        return new H2SpatialExplorerParameters();
573
    }
574

    
575
    @Override
576
    public JDBCServerExplorer createServerExplorer(
577
            JDBCServerExplorerParameters parameters, 
578
            DataServerExplorerProviderServices providerServices
579
        ) throws InitializeException {
580
        
581
        JDBCServerExplorer explorer = new H2SpatialExplorer(
582
                parameters, 
583
                providerServices, 
584
                this
585
        );
586
        this.initialize(explorer, parameters, null);
587
        return explorer;
588
    }
589
    
590
}