001 //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/tags/2.1/src/org/deegree/tools/datastore/DBSchemaToDatastoreConf.java $ 002 /*---------------- FILE HEADER ------------------------------------------ 003 004 This file is part of deegree. 005 Copyright (C) 2001-2007 by: 006 EXSE, Department of Geography, University of Bonn 007 http://www.giub.uni-bonn.de/deegree/ 008 lat/lon GmbH 009 http://www.lat-lon.de 010 011 This library is free software; you can redistribute it and/or 012 modify it under the terms of the GNU Lesser General Public 013 License as published by the Free Software Foundation; either 014 version 2.1 of the License, or (at your option) any later version. 015 016 This library is distributed in the hope that it will be useful, 017 but WITHOUT ANY WARRANTY; without even the implied warranty of 018 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 019 Lesser General Public License for more details. 020 021 You should have received a copy of the GNU Lesser General Public 022 License along with this library; if not, write to the Free Software 023 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 024 025 Contact: 026 027 Andreas Poth 028 lat/lon GmbH 029 Aennchenstr. 19 030 53177 Bonn 031 Germany 032 E-Mail: poth@lat-lon.de 033 034 Prof. Dr. Klaus Greve 035 Department of Geography 036 University of Bonn 037 Meckenheimer Allee 166 038 53115 Bonn 039 Germany 040 E-Mail: greve@giub.uni-bonn.de 041 042 ---------------------------------------------------------------------------*/ 043 package org.deegree.tools.datastore; 044 045 import java.io.File; 046 import java.io.FileWriter; 047 import java.sql.Connection; 048 import java.sql.ResultSet; 049 import java.sql.ResultSetMetaData; 050 import java.sql.SQLException; 051 import java.sql.Statement; 052 import java.util.Properties; 053 054 import org.deegree.datatypes.Types; 055 import org.deegree.framework.log.ILogger; 056 import org.deegree.framework.log.LoggerFactory; 057 import org.deegree.framework.util.StringTools; 058 import org.deegree.io.DBConnectionPool; 059 import org.deegree.io.DBPoolException; 060 import org.deegree.io.shpapi.ShapeFile; 061 062 /** 063 * Example: java -classpath .;deegree.jar;$databasedriver.jar 064 * org.deegree.tools.datastore.DBSchemaToDatastoreConf -tables mytable,myothertable -user dev 065 * -password dev -driver oracle.jdbc.OracleDriver -url jdbc:oracle:thin:@localhost:1521:devs -output 066 * e:/temp/schema.xsd<br> 067 * or for shapefile:<br> 068 * java -classpath .;deegree.jar org.deegree.tools.datastore.DBSchemaToDatastoreConf -url 069 * c:/data/myshape -driver SHAPE -output e:/temp/schema.xsd<br> 070 * 071 * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a> 072 * @author last edited by: $Author: apoth $ 073 * 074 * @version $Revision: 6865 $, $Date: 2007-05-07 17:33:46 +0200 (Mo, 07 Mai 2007) $ 075 */ 076 public class DBSchemaToDatastoreConf { 077 078 private static final ILogger LOG = LoggerFactory.getLogger( DBSchemaToDatastoreConf.class ); 079 080 private String[] tables = null; 081 082 private String user = null; 083 084 private String pw = null; 085 086 private String driver = null; 087 088 private String logon = null; 089 090 private String backend = null; 091 092 private String srs = "Enter A CRS!!!!!!"; 093 094 /** 095 * 096 * @param tables 097 * list of table names used for one featuretype 098 * @param user 099 * database user 100 * @param pw 101 * users password 102 * @param driver 103 * database driver 104 * @param logon 105 * database URL/logon 106 * @param srs 107 */ 108 public DBSchemaToDatastoreConf( String[] tables, String user, String pw, String driver, 109 String logon, String srs ) { 110 this.driver = driver; 111 this.logon = logon; 112 this.pw = pw; 113 this.user = user; 114 this.tables = tables; 115 if ( srs != null ) { 116 this.srs = srs; 117 } 118 119 if ( driver.toUpperCase().indexOf( "ORACLE" ) > -1 ) { 120 backend = "ORACLE"; 121 } else if ( driver.toUpperCase().indexOf( "POSTGRES" ) > -1 ) { 122 backend = "POSTGIS"; 123 } else if ( driver.toUpperCase().contains( "SHAPE" ) ) { 124 backend = "SHAPE"; 125 } else { 126 backend = "GENERICSQL"; 127 } 128 129 } 130 131 /** 132 * creates a schema/datastore configuration for accessin database table through deegree WFS 133 * 134 * @return 135 * @throws Exception 136 */ 137 public String run() 138 throws Exception { 139 StringBuffer sb = new StringBuffer( 1000 ); 140 141 if ( backend.equals( "SHAPE" ) ) { 142 143 // TODO throw RE if tbl.len != 1 144 145 printShapeHeader( sb, tables[0] ); 146 147 File f = new File( tables[0] ); 148 ShapeFile shp = new ShapeFile( f.getAbsolutePath() ); 149 150 printComplextHeader( sb, f.getName() ); 151 152 String[] dataTypes = shp.getDataTypes(); 153 154 printProperty( f.getName(), "GEOM", 2002, "GEOM", -9999, sb ); 155 156 String[] props = shp.getProperties(); 157 for ( int i = 0; i < props.length; i++ ) { 158 int sqlCode = toSQLCode( dataTypes[i] ); 159 printProperty( tables[0], props[i], sqlCode, 160 Types.getTypeNameForSQLTypeCode( sqlCode ), 161 toPrecision( dataTypes[i] ), sb ); 162 } 163 164 printComplexFooter( sb ); 165 166 shp.close(); 167 168 } else { 169 170 printHeader( sb ); 171 172 for ( int k = 0; k < tables.length; k++ ) { 173 System.out.println( driver ); 174 System.out.println( logon ); 175 Connection con = DBConnectionPool.getInstance().acquireConnection( driver, logon, 176 user, pw ); 177 Statement stmt = con.createStatement(); 178 // ensure that we do not get a filled resultset because we just 179 // need the metainformation 180 LOG.logDebug( "read table: ", tables[k] ); 181 ResultSet rs = stmt.executeQuery( "select * from " + tables[k] + " where 1 = 2" ); 182 183 ResultSetMetaData rsmd = rs.getMetaData(); 184 int cols = rsmd.getColumnCount(); 185 186 printComplextHeader( sb, tables[k] ); 187 for ( int i = 0; i < cols; i++ ) { 188 if ( rsmd.getColumnType( i + 1 ) != 2004 ) { 189 int tp = rsmd.getColumnType( i + 1 ); 190 String tpn = Types.getTypeNameForSQLTypeCode( tp ); 191 LOG.logDebug( tables[k] + "." + rsmd.getColumnName( i + 1 ) + ": " + tpn ); 192 // add property just if type != BLOB 193 printProperty( tables[k], rsmd.getColumnName( i + 1 ), 194 rsmd.getColumnType( i + 1 ), tpn, 195 rsmd.getPrecision( i + 1 ), sb ); 196 } else { 197 LOG.logDebug( "skiped: " + tables[k] + '.' + rsmd.getColumnName( i + 1 ) 198 + ": " + rsmd.getColumnTypeName( i + 1 ) ); 199 } 200 } 201 202 DBConnectionPool.getInstance().releaseConnection( con, driver, logon, user, pw ); 203 printComplexFooter( sb ); 204 } 205 } 206 printFooter( sb ); 207 208 return sb.toString(); 209 } 210 211 private int toPrecision( String dbfType ) { 212 int precision = 0; 213 214 if ( dbfType.equalsIgnoreCase( "N" ) ) { 215 precision = 1; 216 } else if ( dbfType.equalsIgnoreCase( "F" ) ) { 217 precision = 2; 218 } 219 220 return precision; 221 } 222 223 private int toSQLCode( String dbfType ) { 224 225 int type = -9999; 226 227 if ( dbfType.equalsIgnoreCase( "C" ) ) { 228 type = Types.VARCHAR; 229 } else if ( dbfType.equalsIgnoreCase( "F" ) || dbfType.equalsIgnoreCase( "N" ) ) { 230 type = Types.NUMERIC; 231 } else if ( dbfType.equalsIgnoreCase( "D" ) || dbfType.equalsIgnoreCase( "M" ) ) { 232 type = Types.DATE; 233 } else if ( dbfType.equalsIgnoreCase( "L" ) ) { 234 type = Types.BOOLEAN; 235 } else if ( dbfType.equalsIgnoreCase( "B" ) ) { 236 type = Types.BLOB; 237 } 238 239 if ( type == -9999 ) { 240 throw new RuntimeException( "Type '" + dbfType + "' is not suported." ); 241 } 242 243 return type; 244 } 245 246 /** 247 * adds the header of the configuration/schema 248 * 249 * @param sb 250 */ 251 private void printHeader( StringBuffer sb ) { 252 253 sb.append( "<xsd:schema targetNamespace=\"http://www.deegree.org/app\" xmlns:gml=\"http://www.opengis.net/gml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:deegreewfs=\"http://www.deegree.org/wfs\" xmlns:ogc=\"http://www.opengis.net/ogc\" xmlns:app=\"http://www.deegree.org/app\" elementFormDefault=\"qualified\" attributeFormDefault=\"unqualified\">\r\n" 254 + " <xsd:import namespace=\"http://www.opengis.net/gml\" schemaLocation=\"http://schemas.opengis.net/gml/3.1.1/base/feature.xsd\"/>\r\n" 255 + " <xsd:import namespace=\"http://www.opengis.net/gml\" schemaLocation=\"http://schemas.opengis.net/gml/3.1.1/base/geometryAggregates.xsd\"/>\r\n" 256 + " <!-- configuration for the persistence backend to be used -->\r\n" 257 + " <xsd:annotation>\r\n" 258 + " <xsd:appinfo>\r\n" 259 + " <deegreewfs:Prefix>app</deegreewfs:Prefix>\r\n" 260 + " <deegreewfs:Backend>" 261 + backend 262 + "</deegreewfs:Backend>\r\n" 263 + " <deegreewfs:DefaultSRS>" 264 + srs 265 + "</deegreewfs:DefaultSRS>\r\n" 266 + " <JDBCConnection xmlns=\"http://www.deegree.org/jdbc\">\r\n" 267 + " <Driver>" 268 + driver 269 + "</Driver>\r\n" 270 + " <Url>" 271 + logon 272 + "</Url>\r\n" 273 + " <User>" 274 + user 275 + "</User>\r\n" 276 + " <Password>" 277 + pw 278 + "</Password>\r\n" 279 + " <SecurityConstraints/>\r\n" 280 + " <Encoding>iso-8859-1</Encoding>\r\n" 281 + " </JDBCConnection>\r\n" 282 + " </xsd:appinfo>\r\n" 283 + " </xsd:annotation>" ); 284 285 } 286 287 /** 288 * adds the header of the configuration/schema 289 * 290 * @param sb 291 */ 292 private void printShapeHeader( StringBuffer sb, String filename ) { 293 294 sb.append( "<xsd:schema targetNamespace=\"http://www.deegree.org/app\" " ).append( 295 "xmlns:gml=\"http://www.opengis.net/gml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " ).append( 296 "xmlns:deegreewfs=\"http://www.deegree.org/wfs\" " ).append( 297 "xmlns:ogc=\"http://www.opengis.net/ogc\" xmlns:app=\"http://www.deegree.org/app\" " ).append( 298 "elementFormDefault=\"qualified\" attributeFormDefault=\"unqualified\">\r\n " ).append( 299 " <xsd:import namespace=\"http://www.opengis.net/gml\" " ).append( 300 "schemaLocation=\"http://schemas.opengis.net/gml/3.1.1/base/feature.xsd\"/>\r\n" ).append( 301 " <xsd:import namespace=\"http://www.opengis.net/gml\" " ).append( 302 "schemaLocation=\"http://schemas.opengis.net/gml/3.1.1/base/geometryAggregates.xsd\"/>\r\n" ).append( 303 " <!-- configuration for the persistence backend to be used -->\r\n" ).append( 304 " <xsd:annotation> " ).append( 305 " <xsd:appinfo>\n" ).append( 306 " <deegreewfs:Prefix>app</deegreewfs:Prefix>\n" ).append( 307 " <deegreewfs:Backend>SHAPE</deegreewfs:Backend>\n" ).append( 308 " <deegreewfs:File>" ).append( 309 filename ).append( 310 "</deegreewfs:File>\n" ).append( 311 " <deegreewfs:DefaultSRS>" 312 + srs 313 + "</deegreewfs:DefaultSRS>\n" ).append( 314 " </xsd:appinfo>\n" ).append( 315 " </xsd:annotation>\n" ); 316 } 317 318 /** 319 * adds a header for a feature type to the schema 320 * 321 * @param sb 322 * @param table 323 * name of the table the feature type is assigned to 324 * @throws Exception 325 */ 326 private void printComplextHeader( StringBuffer sb, String table ) 327 throws Exception { 328 String idField = getPKeyName( table ); 329 String tp = "INTEGER"; 330 if ( backend.equals( "GENERICSQL" ) ) { 331 tp = "VARCHAR"; 332 } 333 sb.append( "<!-- ============================================================== -->\n" 334 + "<xsd:element name='" + table + "' type='app:" + table + "Type' " 335 + "substitutionGroup=\"gml:_Feature\">\r\n" + " " 336 + "<xsd:annotation>\r\n" + " <xsd:appinfo>\r\n" 337 + " <deegreewfs:table>" + table + "</deegreewfs:table>\r\n" 338 + " <deegreewfs:gmlId prefix=\"ID_\">\r\n" 339 + " <deegreewfs:MappingField field='" + idField + "' type=\"" + tp 340 + "\"/>\r\n" + " </deegreewfs:gmlId>\r\n" 341 + " </xsd:appinfo>\r\n" + " </xsd:annotation>\r\n" + "</xsd:element>\r\n" 342 + "<!-- ============================================================== -->\n" 343 + " <xsd:complexType name='" + table + "Type'>\r\n" 344 + " <xsd:complexContent>\r\n" 345 + " <xsd:extension base=\"gml:AbstractFeatureType\">\r\n" 346 + " <xsd:sequence>\r\n" ); 347 } 348 349 /** 350 * adds the footer of a feature type definition 351 * 352 * @param sb 353 */ 354 private void printComplexFooter( StringBuffer sb ) { 355 sb.append( " </xsd:sequence>\r\n" + " </xsd:extension>\r\n" 356 + " </xsd:complexContent>\r\n" + " </xsd:complexType>\r\n" ); 357 } 358 359 private void printFooter( StringBuffer sb ) { 360 sb.append( "</xsd:schema>" ); 361 } 362 363 /** 364 * adds a property assigned to a database table field to the schema 365 * 366 * @param tableName 367 * table name 368 * @param name 369 * property name 370 * @param type 371 * xsd type name 372 * @param typeName 373 * SQL type name 374 * @param precision 375 * number precision if type is a number 376 * @param sb 377 * @throws SQLException 378 * @throws DBPoolException 379 */ 380 private void printProperty( String tableName, String name, int type, String typeName, 381 int precision, StringBuffer sb ) 382 throws DBPoolException, SQLException { 383 384 String tp = Types.getXSDTypeForSQLType( type, precision ); 385 if ( !tp.startsWith( "gml:" ) ) { 386 tp = "xsd:" + tp; 387 } 388 int srid = -1; 389 if ( tp.equals( "gml:GeometryPropertyType" ) ) { 390 typeName = "GEOMETRY"; 391 if ( backend.equals( "ORACLE" ) ) { 392 srid = getOracleSRID( tableName, name ); 393 } else if ( backend.equals( "POSTGIS" ) ) { 394 srid = getPostGisSRID( tableName, name ); 395 } 396 sb.append( "<xsd:element name='" + name.toLowerCase() + "' type='" + tp + "'>\r\n" 397 + " <xsd:annotation>\r\n" + " <xsd:appinfo>\r\n" 398 + " <deegreewfs:Content>\r\n" 399 + " <deegreewfs:MappingField field='" + name + "' type='" 400 + typeName.toUpperCase() + "' srs='" + srid + "'/>\r\n" 401 + " </deegreewfs:Content>\r\n" + " </xsd:appinfo>\r\n" 402 + " </xsd:annotation>\r\n" + "</xsd:element>\r\n" ); 403 } else { 404 sb.append( "<xsd:element name='" + name.toLowerCase() + "' type='" + tp + "'>\r\n" 405 + " <xsd:annotation>\r\n" + " <xsd:appinfo>\r\n" 406 + " <deegreewfs:Content>\r\n" 407 + " <deegreewfs:MappingField field='" + name + "' type='" 408 + typeName.toUpperCase() + "'/>\r\n" 409 + " </deegreewfs:Content>\r\n" + " </xsd:appinfo>\r\n" 410 + " </xsd:annotation>\r\n" + "</xsd:element>\r\n" ); 411 } 412 } 413 414 /** 415 * Retrieve the srid from the postgis database. 416 * 417 * @param tableName 418 * @param name 419 * @return int 420 * @throws DBPoolException 421 * @throws SQLException 422 */ 423 private int getPostGisSRID( String tableName, String name ) 424 throws DBPoolException, SQLException { 425 426 int srid = -1; 427 Connection con = null; 428 try { 429 con = DBConnectionPool.getInstance().acquireConnection( driver, logon, user, pw ); 430 Statement stmt = con.createStatement(); 431 ResultSet rs = stmt.executeQuery( "SELECT a.srid FROM GEOMETRY_COLUMNS a WHERE " 432 + "a.f_table_name='" + tableName.toUpperCase() 433 + "' AND a.f_geometry_column='" + name.toUpperCase() 434 + "'" ); 435 436 while ( rs.next() ) { 437 srid = rs.getInt( 1 ); 438 } 439 440 if ( srid == 0 ) { 441 srid = -1; 442 } 443 444 } catch ( DBPoolException e ) { 445 throw new DBPoolException( 446 "Unable to acquire a connection from the DBConnectionPool for the postgis database. ", 447 e ); 448 } catch ( SQLException e ) { 449 throw new SQLException( 450 "Error performing the postgis query to retrieve the srid from the GEOMETRY_COLUMNS table. " 451 + e ); 452 } finally { 453 DBConnectionPool.getInstance().releaseConnection( con, driver, logon, user, pw ); 454 } 455 return srid; 456 } 457 458 /** 459 * Retrieve the srid from the oracle database. 460 * 461 * @param tableName 462 * @param name 463 * @return int 464 * @throws DBPoolException 465 * @throws SQLException 466 */ 467 private int getOracleSRID( String tableName, String name ) 468 throws DBPoolException, SQLException { 469 470 int srid = -1; 471 Connection con = null; 472 try { 473 con = DBConnectionPool.getInstance().acquireConnection( driver, logon, user, pw ); 474 Statement stmt = con.createStatement(); 475 ResultSet rs = stmt.executeQuery( "SELECT a.srid FROM USER_SDO_GEOM_METADATA a WHERE " 476 + "a.table_name='" + tableName.toUpperCase() 477 + "' AND a.column_name='" + name.toUpperCase() + "'" ); 478 479 while ( rs.next() ) { 480 srid = rs.getInt( 1 ); 481 } 482 if ( srid == 0 ) { 483 srid = -1; 484 } 485 486 } catch ( DBPoolException e ) { 487 throw new DBPoolException( "Unable to acquire a connection from the " 488 + "DBConnectionPool for the oracle database. ", e ); 489 } catch ( SQLException e ) { 490 throw new SQLException( "Error performing the oracle query to retrieve the " 491 + "srid from the GEOMETRY_COLUMNS table. " + e ); 492 } finally { 493 DBConnectionPool.getInstance().releaseConnection( con, driver, logon, user, pw ); 494 } 495 496 return srid; 497 } 498 499 /** 500 * returns the name of the primary key of the passed table 501 * 502 * @param table 503 * @return the name of the primary key of the passed table 504 * @throws Exception 505 */ 506 private String getPKeyName( String table ) 507 throws Exception { 508 if ( backend.equals( "ORACLE" ) ) { 509 return getOraclePKeyName( table ); 510 } else if ( backend.equals( "POSTGIS" ) ) { 511 return getPostgresPKeyName( table ); 512 } else if ( backend.equals( "GENERICSQL" ) ) { 513 return "FEATURE_ID"; 514 } else { 515 return "ID"; 516 } 517 } 518 519 /** 520 * returns the primary key of a table from the oracle database 521 * 522 * @param table 523 * @return the primary key of a table from the oracle database 524 * @throws Exception 525 */ 526 private String getOraclePKeyName( String table ) 527 throws Exception { 528 529 String query = "SELECT cols.column_name " 530 + "FROM all_constraints cons, all_cons_columns cols " 531 + "WHERE cols.table_name = '" + table.toUpperCase() + "' " 532 + "AND cons.constraint_type = 'P' " 533 + "AND cons.constraint_name = cols.constraint_name " 534 + "AND cons.owner = cols.owner " 535 + "ORDER BY cols.table_name, cols.position "; 536 537 Connection con = DBConnectionPool.getInstance().acquireConnection( driver, logon, user, pw ); 538 Statement stmt = con.createStatement(); 539 ResultSet rs = stmt.executeQuery( query ); 540 Object id = null; 541 if ( rs.next() ) { 542 id = rs.getObject( 1 ); 543 } 544 if ( id == null ) { 545 id = "ID"; 546 } 547 DBConnectionPool.getInstance().releaseConnection( con, driver, logon, user, pw ); 548 549 return id.toString(); 550 } 551 552 /** 553 * returns the primary key of a table from the postgres database 554 * 555 * @param table 556 * @return the primary key of a table from the postgres database 557 * @throws Exception 558 */ 559 private String getPostgresPKeyName( String table ) 560 throws Exception { 561 String query = "select b.column_name from pg_catalog.pg_constraint a, " 562 + "information_schema.constraint_column_usage b Where a.conname = " 563 + "b.constraint_name AND a.contype = 'p' AND " + "b.table_name = '" 564 + table.toLowerCase() + "'"; 565 Connection con = DBConnectionPool.getInstance().acquireConnection( driver, logon, user, pw ); 566 Statement stmt = con.createStatement(); 567 ResultSet rs = stmt.executeQuery( query ); 568 Object id = null; 569 if ( rs.next() ) { 570 id = rs.getObject( 1 ); 571 } 572 if ( id == null ) { 573 id = "ID"; 574 } 575 DBConnectionPool.getInstance().releaseConnection( con, driver, logon, user, pw ); 576 return id.toString(); 577 } 578 579 private static void validate( Properties map ) 580 throws Exception { 581 if ( map.get( "-tables" ) == null ) { 582 throw new Exception( "-tables must be set" ); 583 } 584 if ( map.get( "-user" ) == null ) { 585 map.put( "-password", " " ); 586 } 587 if ( map.get( "-password" ) == null ) { 588 map.put( "-password", " " ); 589 } 590 if ( map.get( "-driver" ) == null ) { 591 throw new Exception( "-driver must be set" ); 592 } 593 if ( map.get( "-url" ) == null && !"SHAPE".equalsIgnoreCase( (String) map.get( "-driver" ) ) ) { 594 throw new Exception( "-url (database connection string) must be set" ); 595 } 596 if ( map.get( "-output" ) == null ) { 597 throw new Exception( "-output must be set" ); 598 } 599 } 600 601 /** 602 * @param args 603 * @throws Exception 604 */ 605 public static void main( String[] args ) 606 throws Exception { 607 608 Properties map = new Properties(); 609 for ( int i = 0; i < args.length; i += 2 ) { 610 System.out.println( args[i + 1] ); 611 map.put( args[i], args[i + 1] ); 612 } 613 614 validate( map ); 615 LOG.logInfo( map.toString() ); 616 String tmp = (String) map.get( "-tables" ); 617 String[] tables = StringTools.toArray( tmp, ",;|", true ); 618 String user = (String) map.get( "-user" ); 619 String pw = (String) map.get( "-password" ); 620 String driver = (String) map.get( "-driver" ); 621 String url = (String) map.get( "-url" ); 622 String output = (String) map.get( "-output" ); 623 String srs = (String) map.get( "-srs" ); 624 625 DBSchemaToDatastoreConf stc = new DBSchemaToDatastoreConf( tables, user, pw, driver, url, 626 srs ); 627 628 String conf = stc.run(); 629 FileWriter fw = new FileWriter( output ); 630 fw.write( conf ); 631 fw.close(); 632 System.exit( 0 ); 633 } 634 635 }