1 /* 2 * Copyright (c) 1997, 2005, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 27 package javax.activation; 28 29 import java.util.*; 30 import java.io.*; 31 import java.net.*; 32 import com.sun.activation.registries.MailcapFile; 33 import com.sun.activation.registries.LogSupport; 34 35 /** 36 * MailcapCommandMap extends the CommandMap 37 * abstract class. It implements a CommandMap whose configuration 38 * is based on mailcap files 39 * (<A HREF="http://www.ietf.org/rfc/rfc1524.txt">RFC 1524</A>). 40 * The MailcapCommandMap can be configured both programmatically 41 * and via configuration files. 42 * <p> 43 * <b>Mailcap file search order:</b><p> 44 * The MailcapCommandMap looks in various places in the user's 45 * system for mailcap file entries. When requests are made 46 * to search for commands in the MailcapCommandMap, it searches 47 * mailcap files in the following order: 48 * <p> 49 * <ol> 50 * <li> Programatically added entries to the MailcapCommandMap instance. 51 * <li> The file <code>.mailcap</code> in the user's home directory. 52 * <li> The file <<i>java.home</i>><code>/lib/mailcap</code>. 53 * <li> The file or resources named <code>META-INF/mailcap</code>. 54 * <li> The file or resource named <code>META-INF/mailcap.default</code> 55 * (usually found only in the <code>activation.jar</code> file). 56 * </ol> 57 * <p> 58 * <b>Mailcap file format:</b><p> 59 * 60 * Mailcap files must conform to the mailcap 61 * file specification (RFC 1524, <i>A User Agent Configuration Mechanism 62 * For Multimedia Mail Format Information</i>). 63 * The file format consists of entries corresponding to 64 * particular MIME types. In general, the specification 65 * specifies <i>applications</i> for clients to use when they 66 * themselves cannot operate on the specified MIME type. The 67 * MailcapCommandMap extends this specification by using a parameter mechanism 68 * in mailcap files that allows JavaBeans(tm) components to be specified as 69 * corresponding to particular commands for a MIME type.<p> 70 * 71 * When a mailcap file is 72 * parsed, the MailcapCommandMap recognizes certain parameter signatures, 73 * specifically those parameter names that begin with <code>x-java-</code>. 74 * The MailcapCommandMap uses this signature to find 75 * command entries for inclusion into its registries. 76 * Parameter names with the form <code>x-java-<name></code> 77 * are read by the MailcapCommandMap as identifying a command 78 * with the name <i>name</i>. When the <i>name</i> is <code> 79 * content-handler</code> the MailcapCommandMap recognizes the class 80 * signified by this parameter as a <i>DataContentHandler</i>. 81 * All other commands are handled generically regardless of command 82 * name. The command implementation is specified by a fully qualified 83 * class name of a JavaBean(tm) component. For example; a command for viewing 84 * some data can be specified as: <code>x-java-view=com.foo.ViewBean</code>.<p> 85 * 86 * When the command name is <code>fallback-entry</code>, the value of 87 * the command may be <code>true</code> or <code>false</code>. An 88 * entry for a MIME type that includes a parameter of 89 * <code>x-java-fallback-entry=true</code> defines fallback commands 90 * for that MIME type that will only be used if no non-fallback entry 91 * can be found. For example, an entry of the form <code>text/*; ; 92 * x-java-fallback-entry=true; x-java-view=com.sun.TextViewer</code> 93 * specifies a view command to be used for any text MIME type. This 94 * view command would only be used if a non-fallback view command for 95 * the MIME type could not be found.<p> 96 * 97 * MailcapCommandMap aware mailcap files have the 98 * following general form:<p> 99 * <code> 100 * # Comments begin with a '#' and continue to the end of the line.<br> 101 * <mime type>; ; <parameter list><br> 102 * # Where a parameter list consists of one or more parameters,<br> 103 * # where parameters look like: x-java-view=com.sun.TextViewer<br> 104 * # and a parameter list looks like: <br> 105 * text/plain; ; x-java-view=com.sun.TextViewer; x-java-edit=com.sun.TextEdit 106 * <br> 107 * # Note that mailcap entries that do not contain 'x-java' parameters<br> 108 * # and comply to RFC 1524 are simply ignored:<br> 109 * image/gif; /usr/dt/bin/sdtimage %s<br> 110 * 111 * </code> 112 * <p> 113 * 114 * @author Bart Calder 115 * @author Bill Shannon 116 * 117 * @since 1.6 118 */ 119 120 public class MailcapCommandMap extends CommandMap { 121 /* 122 * We manage a collection of databases, searched in order. 123 * The default database is shared between all instances 124 * of this class. 125 * XXX - Can we safely share more databases between instances? 126 */ 127 private static MailcapFile defDB = null; 128 private MailcapFile[] DB; 129 private static final int PROG = 0; // programmatically added entries 130 131 /** 132 * The default Constructor. 133 */ 134 public MailcapCommandMap() { 135 super(); 136 List dbv = new ArrayList(5); // usually 5 or less databases 137 MailcapFile mf = null; 138 dbv.add(null); // place holder for PROG entry 139 140 LogSupport.log("MailcapCommandMap: load HOME"); 141 try { 142 String user_home = System.getProperty("user.home"); 143 144 if (user_home != null) { 145 String path = user_home + File.separator + ".mailcap"; 146 mf = loadFile(path); 147 if (mf != null) 148 dbv.add(mf); 149 } 150 } catch (SecurityException ex) {} 151 152 LogSupport.log("MailcapCommandMap: load SYS"); 153 try { 154 // check system's home 155 String system_mailcap = System.getProperty("java.home") + 156 File.separator + "lib" + File.separator + "mailcap"; 157 mf = loadFile(system_mailcap); 158 if (mf != null) 159 dbv.add(mf); 160 } catch (SecurityException ex) {} 161 162 LogSupport.log("MailcapCommandMap: load JAR"); 163 // load from the app's jar file 164 loadAllResources(dbv, "META-INF/mailcap"); 165 166 LogSupport.log("MailcapCommandMap: load DEF"); 167 synchronized (MailcapCommandMap.class) { 168 // see if another instance has created this yet. 169 if (defDB == null) 170 defDB = loadResource("/META-INF/mailcap.default"); 171 } 172 173 if (defDB != null) 174 dbv.add(defDB); 175 176 DB = new MailcapFile[dbv.size()]; 177 DB = (MailcapFile[])dbv.toArray(DB); 178 } 179 180 /** 181 * Load from the named resource. 182 */ 183 private MailcapFile loadResource(String name) { 184 InputStream clis = null; 185 try { 186 clis = SecuritySupport.getResourceAsStream(this.getClass(), name); 187 if (clis != null) { 188 MailcapFile mf = new MailcapFile(clis); 189 if (LogSupport.isLoggable()) 190 LogSupport.log("MailcapCommandMap: successfully loaded " + 191 "mailcap file: " + name); 192 return mf; 193 } else { 194 if (LogSupport.isLoggable()) 195 LogSupport.log("MailcapCommandMap: not loading " + 196 "mailcap file: " + name); 197 } 198 } catch (IOException e) { 199 if (LogSupport.isLoggable()) 200 LogSupport.log("MailcapCommandMap: can't load " + name, e); 201 } catch (SecurityException sex) { 202 if (LogSupport.isLoggable()) 203 LogSupport.log("MailcapCommandMap: can't load " + name, sex); 204 } finally { 205 try { 206 if (clis != null) 207 clis.close(); 208 } catch (IOException ex) { } // ignore it 209 } 210 return null; 211 } 212 213 /** 214 * Load all of the named resource. 215 */ 216 private void loadAllResources(List v, String name) { 217 boolean anyLoaded = false; 218 try { 219 URL[] urls; 220 ClassLoader cld = null; 221 // First try the "application's" class loader. 222 cld = SecuritySupport.getContextClassLoader(); 223 if (cld == null) 224 cld = this.getClass().getClassLoader(); 225 if (cld != null) 226 urls = SecuritySupport.getResources(cld, name); 227 else 228 urls = SecuritySupport.getSystemResources(name); 229 if (urls != null) { 230 if (LogSupport.isLoggable()) 231 LogSupport.log("MailcapCommandMap: getResources"); 232 for (int i = 0; i < urls.length; i++) { 233 URL url = urls[i]; 234 InputStream clis = null; 235 if (LogSupport.isLoggable()) 236 LogSupport.log("MailcapCommandMap: URL " + url); 237 try { 238 clis = SecuritySupport.openStream(url); 239 if (clis != null) { 240 v.add(new MailcapFile(clis)); 241 anyLoaded = true; 242 if (LogSupport.isLoggable()) 243 LogSupport.log("MailcapCommandMap: " + 244 "successfully loaded " + 245 "mailcap file from URL: " + 246 url); 247 } else { 248 if (LogSupport.isLoggable()) 249 LogSupport.log("MailcapCommandMap: " + 250 "not loading mailcap " + 251 "file from URL: " + url); 252 } 253 } catch (IOException ioex) { 254 if (LogSupport.isLoggable()) 255 LogSupport.log("MailcapCommandMap: can't load " + 256 url, ioex); 257 } catch (SecurityException sex) { 258 if (LogSupport.isLoggable()) 259 LogSupport.log("MailcapCommandMap: can't load " + 260 url, sex); 261 } finally { 262 try { 263 if (clis != null) 264 clis.close(); 265 } catch (IOException cex) { } 266 } 267 } 268 } 269 } catch (Exception ex) { 270 if (LogSupport.isLoggable()) 271 LogSupport.log("MailcapCommandMap: can't load " + name, ex); 272 } 273 274 // if failed to load anything, fall back to old technique, just in case 275 if (!anyLoaded) { 276 if (LogSupport.isLoggable()) 277 LogSupport.log("MailcapCommandMap: !anyLoaded"); 278 MailcapFile mf = loadResource("/" + name); 279 if (mf != null) 280 v.add(mf); 281 } 282 } 283 284 /** 285 * Load from the named file. 286 */ 287 private MailcapFile loadFile(String name) { 288 MailcapFile mtf = null; 289 290 try { 291 mtf = new MailcapFile(name); 292 } catch (IOException e) { 293 // e.printStackTrace(); 294 } 295 return mtf; 296 } 297 298 /** 299 * Constructor that allows the caller to specify the path 300 * of a <i>mailcap</i> file. 301 * 302 * @param fileName The name of the <i>mailcap</i> file to open 303 * @exception IOException if the file can't be accessed 304 */ 305 public MailcapCommandMap(String fileName) throws IOException { 306 this(); 307 308 if (LogSupport.isLoggable()) 309 LogSupport.log("MailcapCommandMap: load PROG from " + fileName); 310 if (DB[PROG] == null) { 311 DB[PROG] = new MailcapFile(fileName); 312 } 313 } 314 315 316 /** 317 * Constructor that allows the caller to specify an <i>InputStream</i> 318 * containing a mailcap file. 319 * 320 * @param is InputStream of the <i>mailcap</i> file to open 321 */ 322 public MailcapCommandMap(InputStream is) { 323 this(); 324 325 LogSupport.log("MailcapCommandMap: load PROG"); 326 if (DB[PROG] == null) { 327 try { 328 DB[PROG] = new MailcapFile(is); 329 } catch (IOException ex) { 330 // XXX - should throw it 331 } 332 } 333 } 334 335 /** 336 * Get the preferred command list for a MIME Type. The MailcapCommandMap 337 * searches the mailcap files as described above under 338 * <i>Mailcap file search order</i>.<p> 339 * 340 * The result of the search is a proper subset of available 341 * commands in all mailcap files known to this instance of 342 * MailcapCommandMap. The first entry for a particular command 343 * is considered the preferred command. 344 * 345 * @param mimeType the MIME type 346 * @return the CommandInfo objects representing the preferred commands. 347 */ 348 public synchronized CommandInfo[] getPreferredCommands(String mimeType) { 349 List cmdList = new ArrayList(); 350 if (mimeType != null) 351 mimeType = mimeType.toLowerCase(Locale.ENGLISH); 352 353 for (int i = 0; i < DB.length; i++) { 354 if (DB[i] == null) 355 continue; 356 Map cmdMap = DB[i].getMailcapList(mimeType); 357 if (cmdMap != null) 358 appendPrefCmdsToList(cmdMap, cmdList); 359 } 360 361 // now add the fallback commands 362 for (int i = 0; i < DB.length; i++) { 363 if (DB[i] == null) 364 continue; 365 Map cmdMap = DB[i].getMailcapFallbackList(mimeType); 366 if (cmdMap != null) 367 appendPrefCmdsToList(cmdMap, cmdList); 368 } 369 370 CommandInfo[] cmdInfos = new CommandInfo[cmdList.size()]; 371 cmdInfos = (CommandInfo[])cmdList.toArray(cmdInfos); 372 373 return cmdInfos; 374 } 375 376 /** 377 * Put the commands that are in the hash table, into the list. 378 */ 379 private void appendPrefCmdsToList(Map cmdHash, List cmdList) { 380 Iterator verb_enum = cmdHash.keySet().iterator(); 381 382 while (verb_enum.hasNext()) { 383 String verb = (String)verb_enum.next(); 384 if (!checkForVerb(cmdList, verb)) { 385 List cmdList2 = (List)cmdHash.get(verb); // get the list 386 String className = (String)cmdList2.get(0); 387 cmdList.add(new CommandInfo(verb, className)); 388 } 389 } 390 } 391 392 /** 393 * Check the cmdList to see if this command exists, return 394 * true if the verb is there. 395 */ 396 private boolean checkForVerb(List cmdList, String verb) { 397 Iterator ee = cmdList.iterator(); 398 while (ee.hasNext()) { 399 String enum_verb = 400 (String)((CommandInfo)ee.next()).getCommandName(); 401 if (enum_verb.equals(verb)) 402 return true; 403 } 404 return false; 405 } 406 407 /** 408 * Get all the available commands in all mailcap files known to 409 * this instance of MailcapCommandMap for this MIME type. 410 * 411 * @param mimeType the MIME type 412 * @return the CommandInfo objects representing all the commands. 413 */ 414 public synchronized CommandInfo[] getAllCommands(String mimeType) { 415 List cmdList = new ArrayList(); 416 if (mimeType != null) 417 mimeType = mimeType.toLowerCase(Locale.ENGLISH); 418 419 for (int i = 0; i < DB.length; i++) { 420 if (DB[i] == null) 421 continue; 422 Map cmdMap = DB[i].getMailcapList(mimeType); 423 if (cmdMap != null) 424 appendCmdsToList(cmdMap, cmdList); 425 } 426 427 // now add the fallback commands 428 for (int i = 0; i < DB.length; i++) { 429 if (DB[i] == null) 430 continue; 431 Map cmdMap = DB[i].getMailcapFallbackList(mimeType); 432 if (cmdMap != null) 433 appendCmdsToList(cmdMap, cmdList); 434 } 435 436 CommandInfo[] cmdInfos = new CommandInfo[cmdList.size()]; 437 cmdInfos = (CommandInfo[])cmdList.toArray(cmdInfos); 438 439 return cmdInfos; 440 } 441 442 /** 443 * Put the commands that are in the hash table, into the list. 444 */ 445 private void appendCmdsToList(Map typeHash, List cmdList) { 446 Iterator verb_enum = typeHash.keySet().iterator(); 447 448 while (verb_enum.hasNext()) { 449 String verb = (String)verb_enum.next(); 450 List cmdList2 = (List)typeHash.get(verb); 451 Iterator cmd_enum = ((List)cmdList2).iterator(); 452 453 while (cmd_enum.hasNext()) { 454 String cmd = (String)cmd_enum.next(); 455 cmdList.add(new CommandInfo(verb, cmd)); 456 // cmdList.add(0, new CommandInfo(verb, cmd)); 457 } 458 } 459 } 460 461 /** 462 * Get the command corresponding to <code>cmdName</code> for the MIME type. 463 * 464 * @param mimeType the MIME type 465 * @param cmdName the command name 466 * @return the CommandInfo object corresponding to the command. 467 */ 468 public synchronized CommandInfo getCommand(String mimeType, 469 String cmdName) { 470 if (mimeType != null) 471 mimeType = mimeType.toLowerCase(Locale.ENGLISH); 472 473 for (int i = 0; i < DB.length; i++) { 474 if (DB[i] == null) 475 continue; 476 Map cmdMap = DB[i].getMailcapList(mimeType); 477 if (cmdMap != null) { 478 // get the cmd list for the cmd 479 List v = (List)cmdMap.get(cmdName); 480 if (v != null) { 481 String cmdClassName = (String)v.get(0); 482 483 if (cmdClassName != null) 484 return new CommandInfo(cmdName, cmdClassName); 485 } 486 } 487 } 488 489 // now try the fallback list 490 for (int i = 0; i < DB.length; i++) { 491 if (DB[i] == null) 492 continue; 493 Map cmdMap = DB[i].getMailcapFallbackList(mimeType); 494 if (cmdMap != null) { 495 // get the cmd list for the cmd 496 List v = (List)cmdMap.get(cmdName); 497 if (v != null) { 498 String cmdClassName = (String)v.get(0); 499 500 if (cmdClassName != null) 501 return new CommandInfo(cmdName, cmdClassName); 502 } 503 } 504 } 505 return null; 506 } 507 508 /** 509 * Add entries to the registry. Programmatically 510 * added entries are searched before other entries.<p> 511 * 512 * The string that is passed in should be in mailcap 513 * format. 514 * 515 * @param mail_cap a correctly formatted mailcap string 516 */ 517 public synchronized void addMailcap(String mail_cap) { 518 // check to see if one exists 519 LogSupport.log("MailcapCommandMap: add to PROG"); 520 if (DB[PROG] == null) 521 DB[PROG] = new MailcapFile(); 522 523 DB[PROG].appendToMailcap(mail_cap); 524 } 525 526 /** 527 * Return the DataContentHandler for the specified MIME type. 528 * 529 * @param mimeType the MIME type 530 * @return the DataContentHandler 531 */ 532 public synchronized DataContentHandler createDataContentHandler( 533 String mimeType) { 534 if (LogSupport.isLoggable()) 535 LogSupport.log( 536 "MailcapCommandMap: createDataContentHandler for " + mimeType); 537 if (mimeType != null) 538 mimeType = mimeType.toLowerCase(Locale.ENGLISH); 539 540 for (int i = 0; i < DB.length; i++) { 541 if (DB[i] == null) 542 continue; 543 if (LogSupport.isLoggable()) 544 LogSupport.log(" search DB #" + i); 545 Map cmdMap = DB[i].getMailcapList(mimeType); 546 if (cmdMap != null) { 547 List v = (List)cmdMap.get("content-handler"); 548 if (v != null) { 549 String name = (String)v.get(0); 550 DataContentHandler dch = getDataContentHandler(name); 551 if (dch != null) 552 return dch; 553 } 554 } 555 } 556 557 // now try the fallback entries 558 for (int i = 0; i < DB.length; i++) { 559 if (DB[i] == null) 560 continue; 561 if (LogSupport.isLoggable()) 562 LogSupport.log(" search fallback DB #" + i); 563 Map cmdMap = DB[i].getMailcapFallbackList(mimeType); 564 if (cmdMap != null) { 565 List v = (List)cmdMap.get("content-handler"); 566 if (v != null) { 567 String name = (String)v.get(0); 568 DataContentHandler dch = getDataContentHandler(name); 569 if (dch != null) 570 return dch; 571 } 572 } 573 } 574 return null; 575 } 576 577 private DataContentHandler getDataContentHandler(String name) { 578 if (LogSupport.isLoggable()) 579 LogSupport.log(" got content-handler"); 580 if (LogSupport.isLoggable()) 581 LogSupport.log(" class " + name); 582 try { 583 ClassLoader cld = null; 584 // First try the "application's" class loader. 585 cld = SecuritySupport.getContextClassLoader(); 586 if (cld == null) 587 cld = this.getClass().getClassLoader(); 588 Class cl = null; 589 try { 590 cl = cld.loadClass(name); 591 } catch (Exception ex) { 592 // if anything goes wrong, do it the old way 593 cl = Class.forName(name); 594 } 595 if (cl != null) // XXX - always true? 596 return (DataContentHandler)cl.newInstance(); 597 } catch (IllegalAccessException e) { 598 if (LogSupport.isLoggable()) 599 LogSupport.log("Can't load DCH " + name, e); 600 } catch (ClassNotFoundException e) { 601 if (LogSupport.isLoggable()) 602 LogSupport.log("Can't load DCH " + name, e); 603 } catch (InstantiationException e) { 604 if (LogSupport.isLoggable()) 605 LogSupport.log("Can't load DCH " + name, e); 606 } 607 return null; 608 } 609 610 /** 611 * Get all the MIME types known to this command map. 612 * 613 * @return array of MIME types as strings 614 * @since JAF 1.1 615 */ 616 public synchronized String[] getMimeTypes() { 617 List mtList = new ArrayList(); 618 619 for (int i = 0; i < DB.length; i++) { 620 if (DB[i] == null) 621 continue; 622 String[] ts = DB[i].getMimeTypes(); 623 if (ts != null) { 624 for (int j = 0; j < ts.length; j++) { 625 // eliminate duplicates 626 if (!mtList.contains(ts[j])) 627 mtList.add(ts[j]); 628 } 629 } 630 } 631 632 String[] mts = new String[mtList.size()]; 633 mts = (String[])mtList.toArray(mts); 634 635 return mts; 636 } 637 638 /** 639 * Get the native commands for the given MIME type. 640 * Returns an array of strings where each string is 641 * an entire mailcap file entry. The application 642 * will need to parse the entry to extract the actual 643 * command as well as any attributes it needs. See 644 * <A HREF="http://www.ietf.org/rfc/rfc1524.txt">RFC 1524</A> 645 * for details of the mailcap entry syntax. Only mailcap 646 * entries that specify a view command for the specified 647 * MIME type are returned. 648 * 649 * @return array of native command entries 650 * @since JAF 1.1 651 */ 652 public synchronized String[] getNativeCommands(String mimeType) { 653 List cmdList = new ArrayList(); 654 if (mimeType != null) 655 mimeType = mimeType.toLowerCase(Locale.ENGLISH); 656 657 for (int i = 0; i < DB.length; i++) { 658 if (DB[i] == null) 659 continue; 660 String[] cmds = DB[i].getNativeCommands(mimeType); 661 if (cmds != null) { 662 for (int j = 0; j < cmds.length; j++) { 663 // eliminate duplicates 664 if (!cmdList.contains(cmds[j])) 665 cmdList.add(cmds[j]); 666 } 667 } 668 } 669 670 String[] cmds = new String[cmdList.size()]; 671 cmds = (String[])cmdList.toArray(cmds); 672 673 return cmds; 674 } 675 676 /** 677 * for debugging... 678 * 679 public static void main(String[] argv) throws Exception { 680 MailcapCommandMap map = new MailcapCommandMap(); 681 CommandInfo[] cmdInfo; 682 683 cmdInfo = map.getPreferredCommands(argv[0]); 684 System.out.println("Preferred Commands:"); 685 for (int i = 0; i < cmdInfo.length; i++) 686 System.out.println("Command " + cmdInfo[i].getCommandName() + " [" + 687 cmdInfo[i].getCommandClass() + "]"); 688 cmdInfo = map.getAllCommands(argv[0]); 689 System.out.println(); 690 System.out.println("All Commands:"); 691 for (int i = 0; i < cmdInfo.length; i++) 692 System.out.println("Command " + cmdInfo[i].getCommandName() + " [" + 693 cmdInfo[i].getCommandClass() + "]"); 694 DataContentHandler dch = map.createDataContentHandler(argv[0]); 695 if (dch != null) 696 System.out.println("DataContentHandler " + 697 dch.getClass().toString()); 698 System.exit(0); 699 } 700 */ 701 }