001 //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.4_testing/src/org/deegree/framework/util/StringTools.java $ 002 /*---------------------------------------------------------------------------- 003 This file is part of deegree, http://deegree.org/ 004 Copyright (C) 2001-2009 by: 005 Department of Geography, University of Bonn 006 and 007 lat/lon GmbH 008 009 This library is free software; you can redistribute it and/or modify it under 010 the terms of the GNU Lesser General Public License as published by the Free 011 Software Foundation; either version 2.1 of the License, or (at your option) 012 any later version. 013 This library is distributed in the hope that it will be useful, but WITHOUT 014 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 015 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 016 details. 017 You should have received a copy of the GNU Lesser General Public License 018 along with this library; if not, write to the Free Software Foundation, Inc., 019 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 020 021 Contact information: 022 023 lat/lon GmbH 024 Aennchenstr. 19, 53177 Bonn 025 Germany 026 http://lat-lon.de/ 027 028 Department of Geography, University of Bonn 029 Prof. Dr. Klaus Greve 030 Postfach 1147, 53001 Bonn 031 Germany 032 http://www.geographie.uni-bonn.de/deegree/ 033 034 e-mail: info@deegree.org 035 ----------------------------------------------------------------------------*/ 036 package org.deegree.framework.util; 037 038 import java.io.BufferedReader; 039 import java.io.IOException; 040 import java.io.InputStream; 041 import java.io.InputStreamReader; 042 import java.io.StringReader; 043 import java.util.ArrayList; 044 import java.util.HashMap; 045 import java.util.List; 046 import java.util.Locale; 047 import java.util.Map; 048 import java.util.Set; 049 import java.util.StringTokenizer; 050 051 import org.deegree.framework.xml.XMLFragment; 052 import org.deegree.framework.xml.XMLParsingException; 053 import org.deegree.framework.xml.XMLTools; 054 import org.deegree.ogcbase.CommonNamespaces; 055 import org.w3c.dom.Node; 056 import org.xml.sax.SAXException; 057 058 /** 059 * this is a collection of some methods that extends the functionality of the sun-java string class. 060 * 061 * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a> 062 * @author last edited by: $Author: apoth $ 063 * 064 * @version $Revision: 19586 $, $Date: 2009-09-10 17:46:22 +0200 (Do, 10. Sep 2009) $ 065 */ 066 public class StringTools { 067 068 /** 069 * This map is used for methods normalizeString() and initMap(). 070 * 071 * key = locale language, e.g. "de" value = map of substitution rules for this locale 072 */ 073 private static Map<String, Map<String, String>> localeMap; 074 075 /** 076 * concatenates an array of strings using a 077 * 078 * @see StringBuffer 079 * 080 * @param size 081 * estimated size of the target string 082 * @param objects 083 * toString() will be called for each object to append it to the result string 084 * @return the concatenated String 085 */ 086 public static String concat( int size, Object... objects ) { 087 StringBuilder sbb = new StringBuilder( size ); 088 for ( int i = 0; i < objects.length; i++ ) { 089 sbb.append( objects[i] ); 090 } 091 return sbb.toString(); 092 } 093 094 /** 095 * replaces occurences of a string fragment within a string by a new string. 096 * 097 * @param target 098 * is the original string 099 * @param from 100 * is the string to be replaced 101 * @param to 102 * is the string which will used to replace 103 * @param all 104 * if it's true all occurences of the string to be replaced will be replaced. else only the first 105 * occurence will be replaced. 106 * @return the changed target string 107 */ 108 public static String replace( String target, String from, String to, boolean all ) { 109 110 StringBuffer buffer = new StringBuffer( target.length() ); 111 int copyFrom = 0; 112 char[] targetChars = null; 113 int lf = from.length(); 114 int start = -1; 115 do { 116 start = target.indexOf( from ); 117 copyFrom = 0; 118 if ( start == -1 ) { 119 return target; 120 } 121 122 targetChars = target.toCharArray(); 123 while ( start != -1 ) { 124 buffer.append( targetChars, copyFrom, start - copyFrom ); 125 buffer.append( to ); 126 copyFrom = start + lf; 127 start = target.indexOf( from, copyFrom ); 128 if ( !all ) { 129 start = -1; 130 } 131 } 132 buffer.append( targetChars, copyFrom, targetChars.length - copyFrom ); 133 target = buffer.toString(); 134 buffer.delete( 0, buffer.length() ); 135 } while ( target.indexOf( from ) > -1 && to.indexOf( from ) < 0 ); 136 137 return target; 138 } 139 140 /** 141 * parse a string and return its tokens as array 142 * 143 * @param s 144 * string to parse 145 * @param delimiter 146 * delimiter that marks the end of a token 147 * @param deleteDoubles 148 * if it's true all string that are already within the resulting array will be deleted, so that there 149 * will only be one copy of them. 150 * @return an Array of Strings 151 */ 152 public static String[] toArray( String s, String delimiter, boolean deleteDoubles ) { 153 if ( s == null || s.equals( "" ) ) { 154 return new String[0]; 155 } 156 157 StringTokenizer st = new StringTokenizer( s, delimiter ); 158 ArrayList<String> vec = new ArrayList<String>( st.countTokens() ); 159 160 if ( st.countTokens() > 0 ) { 161 for ( int i = 0; st.hasMoreTokens(); i++ ) { 162 String t = st.nextToken(); 163 if ( ( t != null ) && ( t.length() > 0 ) ) { 164 vec.add( t.trim() ); 165 } 166 } 167 } else { 168 vec.add( s ); 169 } 170 171 String[] kw = vec.toArray( new String[vec.size()] ); 172 if ( deleteDoubles ) { 173 kw = deleteDoubles( kw ); 174 } 175 176 return kw; 177 } 178 179 /** 180 * parse a string and return its tokens as typed List. empty fields will be removed from the list. 181 * 182 * @param s 183 * string to parse 184 * @param delimiter 185 * delimiter that marks the end of a token 186 * @param deleteDoubles 187 * if it's true all string that are already within the resulting array will be deleted, so that there 188 * will only be one copy of them. 189 * @return a list of Strings 190 */ 191 public static List<String> toList( String s, String delimiter, boolean deleteDoubles ) { 192 if ( s == null || s.equals( "" ) ) { 193 return new ArrayList<String>(); 194 } 195 196 StringTokenizer st = new StringTokenizer( s, delimiter ); 197 ArrayList<String> vec = new ArrayList<String>( st.countTokens() ); 198 for ( int i = 0; st.hasMoreTokens(); i++ ) { 199 String t = st.nextToken(); 200 if ( ( t != null ) && ( t.length() > 0 ) ) { 201 if ( deleteDoubles ) { 202 if ( !vec.contains( t.trim() ) ) { 203 vec.add( t.trim() ); 204 } 205 } else { 206 vec.add( t.trim() ); 207 } 208 } 209 } 210 211 return vec; 212 } 213 214 /** 215 * transforms a string array to one string. the array fields are separated by the submitted delimiter: 216 * 217 * @param s 218 * stringarray to transform 219 * @param delimiter 220 * @return the String representation of the given array 221 */ 222 public static String arrayToString( String[] s, char delimiter ) { 223 StringBuffer res = new StringBuffer( s.length * 20 ); 224 225 for ( int i = 0; i < s.length; i++ ) { 226 res.append( s[i] ); 227 228 if ( i < ( s.length - 1 ) ) { 229 res.append( delimiter ); 230 } 231 } 232 233 return res.toString(); 234 } 235 236 /** 237 * transforms a list to one string. the array fields are separated by the submitted delimiter: 238 * 239 * @param s 240 * stringarray to transform 241 * @param delimiter 242 * @return the String representation of the given list. 243 */ 244 public static String listToString( List<?> s, char delimiter ) { 245 StringBuffer res = new StringBuffer( s.size() * 20 ); 246 247 for ( int i = 0; i < s.size(); i++ ) { 248 res.append( s.get( i ) ); 249 250 if ( i < ( s.size() - 1 ) ) { 251 res.append( delimiter ); 252 } 253 } 254 255 return res.toString(); 256 } 257 258 /** 259 * transforms a double array to one string. the array fields are separated by the submitted delimiter: 260 * 261 * @param s 262 * string array to transform 263 * @param delimiter 264 * @return the String representation of the given array 265 */ 266 public static String arrayToString( double[] s, char delimiter ) { 267 StringBuffer res = new StringBuffer( s.length * 20 ); 268 269 for ( int i = 0; i < s.length; i++ ) { 270 res.append( Double.toString( s[i] ) ); 271 272 if ( i < ( s.length - 1 ) ) { 273 res.append( delimiter ); 274 } 275 } 276 277 return res.toString(); 278 } 279 280 /** 281 * transforms a float array to one string. the array fields are separated by the submitted delimiter: 282 * 283 * @param s 284 * float array to transform 285 * @param delimiter 286 * @return the String representation of the given array 287 */ 288 public static String arrayToString( float[] s, char delimiter ) { 289 StringBuffer res = new StringBuffer( s.length * 20 ); 290 291 for ( int i = 0; i < s.length; i++ ) { 292 res.append( Float.toString( s[i] ) ); 293 294 if ( i < ( s.length - 1 ) ) { 295 res.append( delimiter ); 296 } 297 } 298 299 return res.toString(); 300 } 301 302 /** 303 * transforms a int array to one string. the array fields are separated by the submitted delimiter: 304 * 305 * @param s 306 * stringarray to transform 307 * @param delimiter 308 * @return the String representation of the given array 309 */ 310 public static String arrayToString( int[] s, char delimiter ) { 311 StringBuffer res = new StringBuffer( s.length * 20 ); 312 313 for ( int i = 0; i < s.length; i++ ) { 314 res.append( Integer.toString( s[i] ) ); 315 316 if ( i < ( s.length - 1 ) ) { 317 res.append( delimiter ); 318 } 319 } 320 321 return res.toString(); 322 } 323 324 /** 325 * clears the begin and end of a string from the strings submitted 326 * 327 * @param s 328 * string to validate 329 * @param mark 330 * string to remove from begin and end of <code>s</code> 331 * @return the substring of the given String without the mark at the and and the begin, and trimmed 332 */ 333 public static String validateString( String s, String mark ) { 334 if ( s == null ) { 335 return null; 336 } 337 338 if ( s.length() == 0 ) { 339 return s; 340 } 341 342 s = s.trim(); 343 344 while ( s.startsWith( mark ) ) { 345 s = s.substring( mark.length(), s.length() ).trim(); 346 } 347 348 while ( s.endsWith( mark ) ) { 349 s = s.substring( 0, s.length() - mark.length() ).trim(); 350 } 351 352 return s; 353 } 354 355 /** 356 * deletes all double entries from the submitted array 357 * 358 * @param s 359 * to remove the doubles from 360 * @return The string array without all doubled values 361 */ 362 public static String[] deleteDoubles( String[] s ) { 363 ArrayList<String> vec = new ArrayList<String>( s.length ); 364 365 for ( int i = 0; i < s.length; i++ ) { 366 if ( !vec.contains( s[i] ) ) { 367 vec.add( s[i] ); 368 } 369 } 370 371 return vec.toArray( new String[vec.size()] ); 372 } 373 374 /** 375 * removes all fields from the array that equals <code>s</code> 376 * 377 * @param target 378 * array where to remove the submitted string 379 * @param s 380 * string to remove 381 * @return the String array with all exact occurrences of given String removed. 382 */ 383 public static String[] removeFromArray( String[] target, String s ) { 384 ArrayList<String> vec = new ArrayList<String>( target.length ); 385 386 for ( int i = 0; i < target.length; i++ ) { 387 if ( !target[i].equals( s ) ) { 388 vec.add( target[i] ); 389 } 390 } 391 392 return vec.toArray( new String[vec.size()] ); 393 } 394 395 /** 396 * checks if the submitted array contains the string <code>value</code> 397 * 398 * @param target 399 * array to check if it contains <code>value</code> 400 * @param value 401 * string to check if it within the array 402 * @return true if the given value is contained (without case comparison) in the array, caution, if the value ends 403 * with a comma ',' a substring will be taken to remove it (rb: For whatever reason??). 404 */ 405 public static boolean contains( String[] target, String value ) { 406 if ( target == null || value == null ) { 407 return false; 408 } 409 410 if ( value.endsWith( "," ) ) { 411 value = value.substring( 0, value.length() - 1 ); 412 } 413 414 for ( int i = 0; i < target.length; i++ ) { 415 if ( value.equalsIgnoreCase( target[i] ) ) { 416 return true; 417 } 418 } 419 420 return false; 421 } 422 423 /** 424 * convert the array of string like [(x1,y1),(x2,y2)...] into an array of double [x1,y1,x2,y2...] 425 * 426 * @param s 427 * @param delimiter 428 * 429 * @return the array representation of the given String 430 */ 431 public static double[] toArrayDouble( String s, String delimiter ) { 432 if ( s == null || "".equals( s.trim() ) ) { 433 return null; 434 } 435 StringTokenizer st = new StringTokenizer( s, delimiter ); 436 437 ArrayList<String> vec = new ArrayList<String>( st.countTokens() ); 438 439 for ( int i = 0; st.hasMoreTokens(); i++ ) { 440 String t = st.nextToken().replace( ' ', '+' ); 441 442 if ( ( t != null ) && ( t.length() > 0 ) ) { 443 vec.add( t.trim().replace( ',', '.' ) ); 444 } 445 } 446 447 double[] array = new double[vec.size()]; 448 449 for ( int i = 0; i < vec.size(); i++ ) { 450 array[i] = Double.parseDouble( vec.get( i ) ); 451 } 452 453 return array; 454 } 455 456 /** 457 * convert the array of string like [(x1,y1),(x2,y2)...] into an array of float values [x1,y1,x2,y2...] 458 * 459 * @param s 460 * @param delimiter 461 * 462 * @return the array representation of the given String 463 */ 464 public static float[] toArrayFloat( String s, String delimiter ) { 465 if ( s == null ) { 466 return null; 467 } 468 469 if ( s.equals( "" ) ) { 470 return null; 471 } 472 473 StringTokenizer st = new StringTokenizer( s, delimiter ); 474 475 ArrayList<String> vec = new ArrayList<String>( st.countTokens() ); 476 for ( int i = 0; st.hasMoreTokens(); i++ ) { 477 String t = st.nextToken().replace( ' ', '+' ); 478 if ( ( t != null ) && ( t.length() > 0 ) ) { 479 vec.add( t.trim().replace( ',', '.' ) ); 480 } 481 } 482 483 float[] array = new float[vec.size()]; 484 485 for ( int i = 0; i < vec.size(); i++ ) { 486 array[i] = Float.parseFloat( vec.get( i ) ); 487 } 488 489 return array; 490 } 491 492 /** 493 * prints current stactrace 494 */ 495 public static void printStacktrace() { 496 System.out.println(StringTools.stackTraceToString( Thread.getAllStackTraces().get( Thread.currentThread() ) )); 497 } 498 499 /** 500 * transforms an array of StackTraceElements into a String 501 * 502 * @param se 503 * to put to String 504 * @return a String representation of the given Stacktrace. 505 */ 506 public static String stackTraceToString( StackTraceElement[] se ) { 507 508 StringBuffer sb = new StringBuffer(); 509 for ( int i = 0; i < se.length; i++ ) { 510 sb.append( se[i].getClassName() + " " ); 511 sb.append( se[i].getFileName() + " " ); 512 sb.append( se[i].getMethodName() + "(" ); 513 sb.append( se[i].getLineNumber() + ")\n" ); 514 } 515 return sb.toString(); 516 } 517 518 /** 519 * Get the message and the class, as well as the stack trace of the passed Throwable and transforms it into a String 520 * 521 * @param e 522 * to get information from 523 * @return the String representation of the given Throwable 524 */ 525 public static String stackTraceToString( Throwable e ) { 526 if ( e == null ) { 527 return "No Throwable given."; 528 } 529 StringBuffer sb = new StringBuffer(); 530 sb.append( e.getMessage() ).append( "\n" ); 531 sb.append( e.getClass().getName() ).append( "\n" ); 532 sb.append( stackTraceToString( e.getStackTrace() ) ); 533 return sb.toString(); 534 } 535 536 /** 537 * countString count the occurrences of token into target 538 * 539 * @param target 540 * @param token 541 * 542 * @return the number of occurrences of the given token in the given String 543 */ 544 public static int countString( String target, String token ) { 545 int start = target.indexOf( token ); 546 int count = 0; 547 548 while ( start != -1 ) { 549 count++; 550 start = target.indexOf( token, start + 1 ); 551 } 552 553 return count; 554 } 555 556 /** 557 * Extract all the strings that begin with "start" and end with "end" and store it into an array of String 558 * 559 * @param target 560 * @param startString 561 * @param endString 562 * 563 * @return <code>null</code> if no strings were found!! 564 */ 565 public static String[] extractStrings( String target, String startString, String endString ) { 566 int start = target.indexOf( startString ); 567 568 if ( start == -1 ) { 569 return null; 570 } 571 572 int count = countString( target, startString ); 573 String[] subString = null; 574 if ( startString.equals( endString ) ) { 575 count = count / 2; 576 subString = new String[count]; 577 for ( int i = 0; i < count; i++ ) { 578 int tmp = target.indexOf( endString, start + 1 ); 579 subString[i] = target.substring( start, tmp + 1 ); 580 start = target.indexOf( startString, tmp + 1 ); 581 } 582 } else { 583 subString = new String[count]; 584 for ( int i = 0; i < count; i++ ) { 585 subString[i] = target.substring( start, target.indexOf( endString, start + 1 ) + 1 ); 586 subString[i] = extractString( subString[i], startString, endString, true, true ); 587 start = target.indexOf( startString, start + 1 ); 588 } 589 } 590 591 return subString; 592 } 593 594 /** 595 * extract a string contained between startDel and endDel, you can remove the delimiters if set true the parameters 596 * delStart and delEnd 597 * 598 * @param target 599 * to extract from 600 * @param startDel 601 * to remove from the start 602 * @param endDel 603 * string to remove from the end 604 * @param delStart 605 * true if the start should be removed 606 * @param delEnd 607 * true if the end should be removed 608 * 609 * @return the extracted string from the given target. rb: Caution this method may not do what it should. 610 */ 611 public static String extractString( String target, String startDel, String endDel, boolean delStart, boolean delEnd ) { 612 if ( target == null ) { 613 return null; 614 } 615 int start = target.indexOf( startDel ); 616 617 if ( start == -1 ) { 618 return null; 619 } 620 621 String s = target.substring( start, target.indexOf( endDel, start + 1 ) + 1 ); 622 623 s = s.trim(); 624 625 if ( delStart ) { 626 while ( s.startsWith( startDel ) ) { 627 s = s.substring( startDel.length(), s.length() ).trim(); 628 } 629 } 630 631 if ( delEnd ) { 632 while ( s.endsWith( endDel ) ) { 633 s = s.substring( 0, s.length() - endDel.length() ).trim(); 634 } 635 } 636 637 return s; 638 } 639 640 /** 641 * Initialize the substitution map with all normalization rules for a given locale and add this map to the static 642 * localeMap. 643 * 644 * @param locale 645 * @throws IOException 646 * @throws SAXException 647 * @throws XMLParsingException 648 */ 649 private static void initMap( String locale ) 650 throws IOException, SAXException, XMLParsingException { 651 652 // read normalization file 653 StringBuffer sb = new StringBuffer( 1000 ); 654 InputStream is = StringTools.class.getResourceAsStream( "/normalization.xml" ); 655 if ( is == null ) { 656 is = StringTools.class.getResourceAsStream( "normalization.xml" ); 657 } 658 BufferedReader br = new BufferedReader( new InputStreamReader( is ) ); 659 String s = null; 660 while ( ( s = br.readLine() ) != null ) { 661 sb.append( s ); 662 } 663 br.close(); 664 665 // transform into xml fragment 666 XMLFragment xml = new XMLFragment(); 667 xml.load( new StringReader( sb.toString() ), StringTools.class.getResource( "normalization.xml" ).toString() ); // FIXME 668 669 // create map 670 Map<String, String> substitutionMap = new HashMap<String, String>( 20 ); 671 672 // extract case attrib ( "toLower" or "toUpper" or missing ) for passed locale 673 String xpath = "Locale[@name = '" + Locale.GERMANY.getLanguage() + "']/@case"; 674 String letterCase = XMLTools.getNodeAsString( xml.getRootElement(), xpath, 675 CommonNamespaces.getNamespaceContext(), null ); 676 if ( letterCase != null ) { 677 substitutionMap.put( "case", letterCase ); 678 } 679 680 // extract removeDoubles attrib ( "true" or "false" ) for passed locale 681 xpath = "Locale[@name = '" + Locale.GERMANY.getLanguage() + "']/@removeDoubles"; 682 String removeDoubles = XMLTools.getNodeAsString( xml.getRootElement(), xpath, 683 CommonNamespaces.getNamespaceContext(), null ); 684 if ( removeDoubles != null && removeDoubles.length() > 0 ) { 685 substitutionMap.put( "removeDoubles", removeDoubles ); 686 } 687 688 // extract rules section for passed locale 689 xpath = "Locale[@name = '" + locale + "']/Rule"; 690 List<Node> list = XMLTools.getNodes( xml.getRootElement(), xpath, CommonNamespaces.getNamespaceContext() ); 691 if ( list != null ) { 692 // for ( int i = 0; i < list.size(); i++ ) { 693 for ( Node n : list ) { 694 String src = XMLTools.getRequiredNodeAsString( n, "Source", CommonNamespaces.getNamespaceContext() ); 695 String target = XMLTools.getRequiredNodeAsString( n, "Target", CommonNamespaces.getNamespaceContext() ); 696 substitutionMap.put( src, target ); 697 } 698 } 699 700 // init localeMap if needed 701 if ( localeMap == null ) { 702 localeMap = new HashMap<String, Map<String, String>>( 20 ); 703 } 704 705 localeMap.put( locale, substitutionMap ); 706 } 707 708 /** 709 * The passed string gets normalized along the rules for the given locale as they are set in the file 710 * "./normalization.xml". If such rules are specified, the following order is obeyed: 711 * 712 * <ol> 713 * <li>if the attribute "case" is set with "toLower" or "toUpper", the letters are switched to lower case or to 714 * upper case respectively.</li> 715 * <li>all rules given in the "Rule" elements are performed.</li> 716 * <li>if the attribute "removeDoubles" is set and not empty, all multi occurences of the letters given in this 717 * attribute are reduced to a single occurence.</li> 718 * </ol> 719 * 720 * @param source 721 * the String to normalize 722 * @param locale 723 * the locale language defining the rules to choose, e.g. "de" 724 * @return the normalized String 725 * @throws IOException 726 * @throws SAXException 727 * @throws XMLParsingException 728 */ 729 public static String normalizeString( String source, String locale ) 730 throws IOException, SAXException, XMLParsingException { 731 732 if ( localeMap == null ) { 733 localeMap = new HashMap<String, Map<String, String>>( 20 ); 734 } 735 Map<String, String> substitutionMap = localeMap.get( locale ); 736 737 if ( substitutionMap == null ) { 738 initMap( locale ); 739 } 740 substitutionMap = localeMap.get( locale ); 741 742 String output = source; 743 Set<String> keys = substitutionMap.keySet(); 744 745 boolean toUpper = false; 746 boolean toLower = false; 747 boolean removeDoubles = false; 748 749 for ( String key : keys ) { 750 if ( "case".equals( key ) ) { 751 toUpper = "toUpper".equals( substitutionMap.get( key ) ); 752 toLower = "toLower".equals( substitutionMap.get( key ) ); 753 } 754 if ( "removeDoubles".equals( key ) && substitutionMap.get( key ).length() > 0 ) { 755 removeDoubles = true; 756 } 757 } 758 759 // first: change letters to upper / lower case 760 if ( toUpper ) { 761 output = output.toUpperCase(); 762 } else if ( toLower ) { 763 output = output.toLowerCase(); 764 } 765 766 // second: change string according to specified rules 767 for ( String key : keys ) { 768 if ( !"case".equals( key ) && !"removeDoubles".equals( key ) ) { 769 output = output.replaceAll( key, substitutionMap.get( key ) ); 770 } 771 } 772 773 // third: remove doubles 774 if ( removeDoubles ) { 775 String doubles = substitutionMap.get( "removeDoubles" ); 776 for ( int i = 0; i < doubles.length(); i++ ) { 777 String remove = "" + doubles.charAt( i ) + "+"; 778 String replaceWith = "" + doubles.charAt( i ); 779 output = output.replaceAll( remove, replaceWith ); 780 } 781 } 782 return output; 783 } 784 }