· 8 years ago · Dec 17, 2017, 11:32 PM
1package com.appiancorp.ps.plugins.mysqldumptodocument;
2
3import java.io.BufferedWriter;
4import java.io.File;
5import java.io.FileWriter;
6import java.io.IOException;
7import java.sql.Connection;
8import java.sql.DatabaseMetaData;
9import java.sql.PreparedStatement;
10import java.sql.ResultSet;
11import java.sql.ResultSetMetaData;
12import java.sql.SQLException;
13import java.util.ArrayList;
14import java.util.Arrays;
15import java.util.List;
16
17import javax.naming.Context;
18import javax.naming.NamingException;
19import javax.sql.DataSource;
20
21import org.apache.log4j.Logger;
22
23import com.appiancorp.suiteapi.common.Name;
24
25import com.appiancorp.suiteapi.content.Content;
26import com.appiancorp.suiteapi.content.ContentConstants;
27import com.appiancorp.suiteapi.content.ContentFilter;
28import com.appiancorp.suiteapi.content.ContentService;
29import com.appiancorp.suiteapi.content.exceptions.InvalidContentException;
30import com.appiancorp.suiteapi.knowledge.Document;
31import com.appiancorp.suiteapi.knowledge.DocumentDataType;
32import com.appiancorp.suiteapi.knowledge.FolderDataType;
33import com.appiancorp.suiteapi.process.exceptions.SmartServiceException;
34import com.appiancorp.suiteapi.process.framework.AppianSmartService;
35import com.appiancorp.suiteapi.process.framework.Input;
36import com.appiancorp.suiteapi.process.framework.MessageContainer;
37import com.appiancorp.suiteapi.process.framework.Required;
38import com.appiancorp.suiteapi.process.palette.ConnectivityServices;
39
40@ConnectivityServices
41public class MySQLDumpToDocument extends AppianSmartService {
42
43 private static final Logger LOG = Logger.getLogger(MySQLDumpToDocument.class);
44 private final ContentService contentService;
45 private Context ctx;
46 private double gbFactor = 1024.0*1024.0*1024.0;
47 private String jndiName;
48 private Long document;
49 private Long folder;
50 private String documentName;
51 private String[] dumpTablePrefixes;
52 private Long dumpDocument;
53 private String errorMessage;
54 private Long batchSize;
55 private File file;
56 private BufferedWriter bw;
57
58 /*
59 * (non-Javadoc)
60 *
61 * @see com.appiancorp.suiteapi.process.framework.AppianSmartService#run()
62 */
63 @Override
64 public void run() throws SmartServiceException {
65 String schemaName = null;
66 Document doc = null;
67 List<String> tableList = null;
68 DataSource datasource = null;
69 Boolean dumpTablePrefixesIsEmpty = false;
70
71 // change according to the process completion times & performance on servers
72 int batchSizePrimitive = 0;
73 if (batchSize != null) {
74 batchSizePrimitive = batchSize.intValue();
75 if (batchSizePrimitive < 10000) {
76 // may effect the performance if it is less than 10,000
77 batchSizePrimitive = 10000;
78 }
79 } else {// if nothing provided assuming it to be 50,000
80 batchSizePrimitive = 50000;
81 }
82
83 // validate plugin parameters
84 schemaName = initialParameterValidtion();
85
86 // create new document or use provided document and update meta data and version
87 doc = createOrUpdateDocument();
88
89 // get file name from the plug-in parameters
90 try {
91 file = new File(getDocumentFile(doc));
92 initializeBufferedWriter();
93 } catch (Exception e) {
94 errorMessage = "Failed to create file due to errors: " + e;
95 throw createException(null, "error.file_creation");
96 }
97
98 // get datasource from the given schema
99 try {
100 datasource = (DataSource) ctx.lookup("jdbc/" + schemaName);
101 } catch (NamingException e) {
102 errorMessage = "Failed to lookup datasource due to errors: "
103 + e.getMessage();
104 throw createException(e, "error.connection");
105 }
106
107 try {
108 if (dumpTablePrefixes == null || dumpTablePrefixes.length <= 0) {
109 dumpTablePrefixesIsEmpty = true;
110 errorMessage = "ERROR: Null value in 'Dump Table Prefixes' input: Please write the table(s) you want to dump in the 'Dump Table Prefixes' input";
111 LOG.error(this.errorMessage);
112 writeTextToFile(errorMessage);
113 } else {
114 if (LOG.isDebugEnabled()) {
115 LOG.debug("Selected table prefixes:");
116 for (String s : Arrays.asList(dumpTablePrefixes)) {
117 LOG.debug(s + ",");
118 }
119 }
120
121 // list of table names
122 tableList = getAllTableNames(datasource);
123
124 // loop through all the TABLES
125 dumpTableCreateStmts(datasource, tableList, schemaName);
126
127 // figure out best buffer size
128 for (String tableName : tableList) {
129 try{
130
131 dumpTableInsertStmts(datasource, tableName, batchSizePrimitive);
132
133 } catch(IllegalStateException e) {
134 LOG.error(e);
135 throw createException(e, "error.memory_limit_exceeded");
136 }
137 }
138
139 // Set the returning Document to the written Document
140 dumpDocument = doc.getId();
141 }
142
143 } catch (IOException e) {
144 errorMessage = "Dump process failed due to errors: " + e.getMessage();
145 throw createException(e, "error.dump_file");
146 } finally {
147 closeBufferedWriter();
148 try {
149 // Update the Appian document meta data with the file size
150 Long currentVersion = contentService.getVersionId(doc.getId(), ContentConstants.VERSION_CURRENT);
151 contentService.setSizeOfDocumentVersion(currentVersion);
152 } catch(Exception e){
153
154 }
155
156 if(dumpTablePrefixesIsEmpty){
157 LOG.info("Please review your inputs.");
158 }else{
159 LOG.info("The file has been created successfully!");
160 }
161 }
162
163 }
164
165 private ResultSet getTableOrViewList(ResultSet rs, List<String> viewList)
166 throws SQLException {
167 String tableName = "";
168 boolean validTableToDump = false;
169
170 if (!rs.next()) {
171 LOG.error("No views found in the database!!!");
172 errorMessage = "Unable to find any views.";
173 } else {
174 do {
175 tableName = rs.getString("TABLE_NAME");
176 validTableToDump = stringStartswithItemFromList(
177 tableName.toLowerCase(), dumpTablePrefixes);
178
179 if (validTableToDump) {
180 viewList.add(tableName);
181 }
182 } while (rs.next());
183 }
184 rs.close();
185 rs = null;
186 return rs;
187 }
188
189 private static boolean stringStartswithItemFromList(String inputString,
190 String[] items) {
191 for (int i = 0; i < items.length; i++) {
192 if (inputString.startsWith(items[i].toLowerCase()))// ignoring the
193 // case of
194 // prefixes
195 {
196 return true;
197 }
198 }
199 return false;
200 }
201
202 private Connection getConnection(DataSource ds) throws SQLException {
203 Connection connection = null;
204
205 if (ds != null) {
206 connection = ds.getConnection();
207 } else {
208 LOG.error("Datasource object is null!!!");
209 errorMessage = "Failed to lookup datasource.";
210 }
211 return connection;
212 }
213
214 private void dumpTableCreateStmts(DataSource datasource,
215 List<String> tableList, String schemaName)
216 throws SmartServiceException, IOException {
217 Connection connection = null;
218
219
220 try {
221 connection = getConnection(datasource);
222
223 // append the crate table statement
224
225 initialDumpFileComments(schemaName);
226
227 for (String tableName : tableList) {
228 PreparedStatement ps = null;
229 ResultSet rs = null;
230
231 try {
232 ps = connection.prepareStatement("show create table "
233 + tableName);
234 rs = ps.executeQuery();
235 createTableStatement(rs, tableName);
236 rs.close();
237 rs = null;
238 ps.close();
239 ps = null;
240
241 } catch (SQLException e) {
242 errorMessage = "SQL Exception while extracting create table statments:"
243 + e.getMessage();
244 throw createException(e, "error.dump_file");
245
246 } finally {
247 closeAllDBObjects(null, ps, rs);
248 }
249 }
250 } catch (Exception e) {
251 errorMessage = "Dump process failed while writing the create table statments due to errors"
252 + e.getMessage();
253 throw createException(e, "error.dump_file");
254
255 } finally {
256 closeAllDBObjects(connection, null, null);
257 }
258 }
259
260 private void dumpTableInsertStmts(DataSource datasource, String tableName, int batchSizePrimitive) throws IOException, SmartServiceException {
261
262 int tableCount = 0, noOfLoops = 0, startIndex = 0;
263 Connection connection = null;
264
265 try {
266 connection = getConnection(datasource);
267
268 // get table count
269 tableCount = getTableRowCount(tableName, connection);
270 noOfLoops = (tableCount / batchSizePrimitive) + 1;
271 LOG.debug("\n*****Insert table:" + tableName + "and tableCount:"+tableCount);
272
273 // loop through all the data subsets form the table
274
275 for (int i = 0; i < noOfLoops; i++) {
276
277 double freeMemory = Runtime.getRuntime().freeMemory();
278 double maxHeap = Runtime.getRuntime().maxMemory();
279
280 if(LOG.isDebugEnabled()){
281
282 LOG.debug("Free heap: " + (freeMemory/gbFactor)/1.00 + "GB ("+ ((freeMemory / maxHeap) * 100) + "%)");
283 }
284
285 if ( (freeMemory / maxHeap) <= 0.2) {
286 throw new IllegalStateException("Error generating dump. Memory threshold reached. Stopped at table {" + tableName + "} row {" + ((startIndex - batchSizePrimitive) > 0 ? (startIndex - batchSizePrimitive) : 0)+ "}");
287 }
288
289
290 PreparedStatement ps = null;
291 ResultSet rs = null;
292
293 long startTime, endTime;
294
295 try {
296 // if all the rows are read then exist the loop
297 if (tableCount <= startIndex) {
298 break;
299 } else {
300
301 if (i == 0) {
302 writeTextToFile("\n\n-- ----Dumping data for:"
303 + tableName.toUpperCase() + "-----\n");
304 writeTextToFile("LOCK TABLES `" + tableName
305 + "` WRITE;\n\n");
306 }
307
308 // append all the table data with insert statements
309 ps = connection.prepareStatement("SELECT * FROM "
310 + tableName + " LIMIT ?, ?");
311 ps.setInt(1, startIndex);
312 ps.setInt(2, batchSizePrimitive);
313 ps.setFetchSize(batchSizePrimitive);
314
315 rs = ps.executeQuery();
316
317 startTime = System.nanoTime();
318 dumpTableData(rs, tableName);
319 endTime = System.nanoTime();
320 LOG.debug("#####time to pull " + batchSize
321 + " records from DB:" + (endTime - startTime));
322
323 rs.close();
324 rs = null;
325 ps.close();
326 ps = null;
327
328 if (i == noOfLoops - 1) {
329 writeTextToFile(";\n\nUNLOCK TABLES;\n");
330 }
331
332 startTime = System.nanoTime();
333
334 endTime = System.nanoTime();
335 LOG.debug("#####time to write " + batchSize
336 + " records into a file:"
337 + (endTime - startTime));
338
339 }
340 startIndex = startIndex + batchSizePrimitive;
341 } finally {
342 closeAllDBObjects(null, ps, rs);
343 }
344 }
345
346 } catch (SQLException e) {
347 errorMessage = "Dump process failed in the insert table statements:" + e.getMessage();
348 throw createException(e, "error.dump_file_insert");
349 } finally {
350 if (connection != null) {
351 try {
352 connection.close();
353 connection = null;
354 } catch (SQLException e) {
355 LOG.error("Not able to close connection!!!");
356 errorMessage = "Failed to close the connection." + e;
357 }
358 }
359 }
360 }
361
362 private void closeAllDBObjects(Connection connection, PreparedStatement ps,
363 ResultSet rs)
364 throws IOException {
365
366 if (rs != null) {
367 try {
368 rs.close();
369 rs = null;
370 } catch (Exception e) {
371 LOG.error("Not able to close resultset!!!");
372 errorMessage = "Failed to close the connection." + e;
373 }
374 }
375 if (ps != null) {
376 try {
377 ps.close();
378 ps = null;
379 } catch (Exception e) {
380 LOG.error("Not able to close statement object!!!");
381 errorMessage = "Failed to close the connection." + e;
382 }
383 }
384 if (connection != null) {
385 try {
386 connection.close();
387 connection = null;
388 } catch (SQLException e) {
389 LOG.error("Not able to close connection!!!");
390 errorMessage = "Failed to close the connection." + e;
391 }
392 }
393 }
394
395 private String initialParameterValidtion() throws SmartServiceException {
396 String schemaName;
397 // Check provided JNDI name for JDBC prefix text
398 if (jndiName.startsWith("jdbc/")) {
399 schemaName = jndiName.substring(5);
400 } else {
401 schemaName = jndiName;
402 }
403 LOG.info("JNDI Connection String Name : " + this.jndiName);
404 LOG.info("Context String Name : " + ctx.toString());
405
406 // Check input parameters for a provided Document or a Folder and
407 // Document Name combination
408 if (document == null && (folder == null || documentName == null)) {
409 throw createException(null, "error.parameters");
410 }
411 return schemaName;
412 }
413
414 private Document createOrUpdateDocument() throws SmartServiceException {
415 String extension = "sql";
416
417 try {
418 if (documentName.endsWith(".sql")) {
419 documentName = documentName.substring(0,
420 documentName.lastIndexOf('.'));
421 }
422 LOG.info("Document Name: " + documentName);
423 ContentFilter cf = new ContentFilter(ContentConstants.TYPE_DOCUMENT);
424 cf.setName(documentName);
425 cf.setExtension(new String[] { extension });
426
427 // Set the document description
428 String descName = jndiName;
429 if (jndiName.contains("jdbc/")) {
430 descName = jndiName.replace("jdbc/", "");
431 }
432 String documentDescription = "MySQL dump of " + descName
433 + " data source.";
434
435 // Update version if document exists or create new document in
436 // provided
437 // folder
438 Long docId = null;
439 if (document != null) {
440 Document d = (Document) contentService.getVersion(document,
441 ContentConstants.VERSION_CURRENT);
442 contentService.createVersion(d,
443 ContentConstants.UNIQUE_FOR_PARENT);
444 docId = d.getId();
445 } else {
446 Content[] children = contentService.getChildren(folder, cf,
447 ContentConstants.GC_MOD_NORMAL);
448 if (children == null || children.length == 0) {
449 Document d = new Document();
450 d.setName(documentName);
451 d.setExtension(extension);
452 d.setSize(1);
453 d.setParent(folder);
454 d.setState(ContentConstants.STATE_ACTIVE_PUBLISHED);
455 d.setDescription(documentDescription);
456 docId = contentService.create(d,
457 ContentConstants.UNIQUE_FOR_PARENT);
458 } else {
459 Document d = (Document) children[0];
460 contentService.createVersion(d,
461 ContentConstants.UNIQUE_FOR_PARENT);
462 docId = d.getId();
463 }
464 }
465
466 if (docId != null) {
467 Document d = contentService.download(docId,
468 ContentConstants.VERSION_CURRENT, false)[0];
469 return d;
470 }
471 return null;
472 } catch (Exception e) {
473 errorMessage = "Failed to create Appian Document due to errors: "
474 + e.getMessage();
475 throw createException(null, "error.document_creation");
476 }
477 }
478
479 private List<String> getAllTableNames(DataSource ds)
480 throws SmartServiceException, IOException {
481 List<String> tableList;
482 Connection connection = null;
483 ResultSet rs = null;
484 DatabaseMetaData metaData = null;
485
486 try {
487 tableList = new ArrayList<String>(50);
488
489 connection = getConnection(ds);
490
491 metaData = connection.getMetaData();
492 String[] VIEW_TYPES = { "TABLE", "VIEW" };
493 rs = metaData.getTables(null, null, null, VIEW_TYPES);
494
495 getTableOrViewList(rs, tableList);
496
497 return tableList;
498
499 } catch (SQLException e) {
500 errorMessage = "Failed to get all the table names due to the errors: "
501 + e.getMessage();
502 throw createException(e, "error.find_table_names");
503 } finally {
504 closeAllDBObjects(connection, null, rs);
505
506 }
507 }
508
509 private String getDocumentFile(Document doc) throws InvalidContentException {
510 String pathname;
511 String filename;
512
513 pathname = contentService.getInternalFilename(doc.getId()).replace(
514 '\\', '/');
515
516 // Set filename based on document file created in Appian
517 filename = pathname.substring(pathname.lastIndexOf('/') + 1);
518 // Change pathname to not include the filename
519 pathname = pathname.substring(0, pathname.lastIndexOf('/') + 1);
520
521 // Write sqlScript to document file location
522 LOG.info("Appian Document Internal Filename: " + pathname + filename);
523
524 return pathname + filename;
525 }
526
527 private int getTableRowCount(String tableName, Connection con)
528 throws SmartServiceException {
529 PreparedStatement ps = null;
530 ResultSet rs = null;
531 int maxRows = 0;
532
533 try {
534 // Get the table row count
535 ps = con.prepareStatement("SELECT count(*) FROM " + tableName);
536 rs = ps.executeQuery();
537 while (rs.next()) {
538 maxRows = rs.getInt(1);
539 }
540
541 } catch (SQLException e) {
542 errorMessage = "Failed to get " + tableName
543 + "table count due to errors: " + e.getMessage();
544 throw createException(e, "error.table_count");
545
546 } finally {
547 if (rs != null) {
548 try {
549 rs.close();
550 } catch (Exception e) {
551 LOG.error("Not able to close resultset!!!");
552 errorMessage = "Failed to close the connection." + e;
553 }
554 }
555 if (ps != null) {
556 try {
557 ps.close();
558 } catch (Exception e) {
559 LOG.error("Not able to close statement object!!!");
560 errorMessage = "Failed to close the connection." + e;
561 }
562 }
563
564 }
565 return maxRows;
566 }
567
568 private void initialDumpFileComments(String schemaName) throws IOException {
569 writeTextToFile("-- ------------------------------------------------\n");
570 writeTextToFile("-- Appian Corporation - SQL Dump to Document File \n");
571 writeTextToFile("-- Dump of " + schemaName + "\n");
572 writeTextToFile("-- ------------------------------------------------\n");
573
574 writeTextToFile("\nCREATE DATABASE IF NOT EXISTS " + schemaName
575 + " /*!40100 DEFAULT CHARACTER SET utf8 */;\nUSE " + schemaName
576 + ";\n");
577
578 writeTextToFile("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n"
579 + "/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n"
580 + "/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n"
581 + "/*!40101 SET NAMES utf8 */;\n"
582 + "/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n"
583 + "/*!40103 SET TIME_ZONE='+00:00' */;\n"
584 + "/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n"
585 + "/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n"
586 + "/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n"
587 + "/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n");
588
589 writeTextToFile("\n\n-- --------------CREATE TABLE STATEMENTS----------------\n\n");
590 }
591
592 private void createTableStatement(ResultSet createTableSet,
593 String tableName) throws SQLException, IOException {
594
595 LOG.info("Table Name: " + tableName);
596
597 // Check if result set has any rows
598 if (createTableSet.next()) {
599 // Get first row, second column which contains create table
600 // statement
601 String sqlCreate = createTableSet.getString(2);
602
603 // Table Information label
604 writeTextToFile("\n\n-- ----Table Structure for table: "
605 + tableName + "------\n");
606
607 // Add drop table statement
608 writeTextToFile("DROP TABLE IF EXISTS `" + tableName + "`;\n");
609
610 // Add create table statement to writeTextToFile(sqlCreate);
611 writeTextToFile(sqlCreate);
612 writeTextToFile(";\n");
613 }
614
615 }
616
617 private void dumpTableData(ResultSet rs, String tableName) throws SQLException, IOException {
618 ResultSetMetaData metaData = null;
619 int columnCount = 0;
620
621 metaData = rs.getMetaData();
622 columnCount = metaData.getColumnCount();
623 LOG.info(tableName + " table column count: " + columnCount);
624
625 // Output of table data
626 if (rs.next()) {
627 writeTextToFile("INSERT INTO " + tableName + " VALUES (");
628
629 int j = 1;
630 do {
631 if (j != 1)
632 writeTextToFile(", (");
633
634 for (int i = 0; i < columnCount; i++) {
635 if (i > 0) {
636 writeTextToFile(", ");
637 }
638
639 String value = null;
640
641 try {
642 value = rs.getString(i + 1);
643 } catch (SQLException e) {
644 if (e.getMessage()
645 .equalsIgnoreCase(
646 "Value \'0000-00-00 00:00:00\' can not be represented as java.sql.Timestamp")) {
647 value = "0000-00-00 00:00:00";
648 } else if (e.getMessage().equalsIgnoreCase(
649 "Value \'0000-00-00\' can not be represented as java.sql.Timestamp")) {
650 value = "0000-00-00";
651 // }else{
652 // throw new SQLException(e);
653 }
654 }
655
656 if (value == null) {
657 writeTextToFile("NULL");
658 } else {
659 char ctrlz = 26;
660 value = value.replaceAll("\\\\", "\\\\\\\\");
661 value = value.replaceAll(Character.toString('\0'), "\\\\0");
662 value = value.replaceAll("\'", "\\\\'");
663 value = value.replaceAll("\"", "\\\\\"");
664 value = value.replaceAll("\b", "\\\\b");
665 value = value.replaceAll("\n", "\\\\n");
666 value = value.replaceAll("\r", "\\\\r");
667 value = value.replaceAll("\t", "\\\\t");
668 value = value.replaceAll("\t", "\\\\t");
669 value = value.replaceAll(Character.toString(ctrlz),"\\\\Z");
670 writeTextToFile("'" + value + "'");
671 }
672 }
673 writeTextToFile(")");
674 j++;
675 } while (rs.next());
676 writeTextToFile(";\n");
677 }
678 }
679
680 public MySQLDumpToDocument(ContentService cs, Context ctx) {
681 super();
682 this.contentService = cs;
683 this.ctx = ctx;
684 }
685
686 public void onSave(MessageContainer messages) {
687 }
688
689 public void validate(MessageContainer messages) {
690 }
691
692 @Input(required = Required.ALWAYS)
693 @Name("jndiName")
694 public void setJndiName(String val) {
695 this.jndiName = val;
696 }
697
698 @Input(required = Required.OPTIONAL)
699 @Name("document")
700 @DocumentDataType
701 public void setDocument(Long val) {
702 this.document = val;
703 }
704
705 @Input(required = Required.OPTIONAL)
706 @Name("folder")
707 @FolderDataType
708 public void setFolder(Long val) {
709 this.folder = val;
710 }
711
712 @Input(required = Required.OPTIONAL)
713 @Name("documentName")
714 public void setDocumentName(String val) {
715 this.documentName = val;
716 }
717
718 @Input(required = Required.OPTIONAL)
719 @Name("dumpTablePrefixes")
720 public void setDumpTablePrefixes(String[] val) {
721 this.dumpTablePrefixes = val;
722 }
723
724 @Input(required = Required.OPTIONAL)
725 @Name("batchSize")
726 public void setBatchSize(Long val) {
727 this.batchSize = val;
728 }
729
730 @Name("dumpDocument")
731 @DocumentDataType
732 public Long getDumpDocument() {
733 return dumpDocument;
734 }
735
736 @Name("errorMessage")
737 public String getErrorMessage() {
738 return errorMessage;
739 }
740
741 private SmartServiceException createException(Throwable t, String key, Object... args) {
742 return new SmartServiceException.Builder(getClass(), t).userMessage(key, args).build();
743 }
744
745 private void writeTextToFile(String text) throws IOException {
746 bw.write(text);
747 }
748
749 private void initializeBufferedWriter () throws IOException {
750 bw = new BufferedWriter(new FileWriter(file));
751 }
752
753 private void closeBufferedWriter(){
754 try {
755 bw.flush();
756 bw.close();
757 } catch (IOException e) {
758 LOG.error("Error closing file", e);
759 }
760 }
761}