1 /* 2 * Copyright (c) 2015, 2016, 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 package jdk.tools.jlink.internal; 26 27 import java.io.File; 28 import java.io.IOException; 29 import java.io.PrintWriter; 30 import java.lang.module.Configuration; 31 import java.lang.module.ModuleFinder; 32 import java.lang.reflect.Layer; 33 import java.nio.file.Files; 34 import java.nio.file.Path; 35 import java.nio.file.Paths; 36 import java.text.MessageFormat; 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.Collections; 40 import java.util.HashMap; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Locale; 44 import java.util.Map; 45 import java.util.Map.Entry; 46 import java.util.MissingResourceException; 47 import java.util.ResourceBundle; 48 import java.util.Set; 49 50 import jdk.internal.module.ConfigurableModuleFinder; 51 import jdk.internal.module.ConfigurableModuleFinder.Phase; 52 import jdk.tools.jlink.Jlink; 53 import jdk.tools.jlink.Jlink.PluginsConfiguration; 54 import jdk.tools.jlink.plugin.Plugin; 55 import jdk.tools.jlink.plugin.Plugin.CATEGORY; 56 import jdk.tools.jlink.builder.DefaultImageBuilder; 57 import jdk.tools.jlink.builder.ImageBuilder; 58 import jdk.tools.jlink.plugin.PluginException; 59 import jdk.tools.jlink.internal.plugins.PluginsResourceBundle; 60 import jdk.tools.jlink.internal.plugins.DefaultCompressPlugin; 61 import jdk.tools.jlink.internal.plugins.StripDebugPlugin; 62 63 /** 64 * 65 * JLink and JImage tools shared helper. 66 */ 67 public final class TaskHelper { 68 69 public static final String JLINK_BUNDLE = "jdk.tools.jlink.resources.jlink"; 70 public static final String JIMAGE_BUNDLE = "jdk.tools.jimage.resources.jimage"; 71 72 private static final String DEFAULTS_PROPERTY = "jdk.jlink.defaults"; 73 74 public final class BadArgs extends Exception { 75 76 static final long serialVersionUID = 8765093759964640721L; 77 78 private BadArgs(String key, Object... args) { 79 super(bundleHelper.getMessage(key, args)); 80 this.key = key; 81 this.args = args; 82 } 83 84 public BadArgs showUsage(boolean b) { 85 showUsage = b; 86 return this; 87 } 88 public final String key; 89 public final Object[] args; 90 public boolean showUsage; 91 } 92 93 public static class Option<T> { 94 95 public interface Processing<T> { 96 97 void process(T task, String opt, String arg) throws BadArgs; 98 } 99 100 final boolean hasArg; 101 final Processing<T> processing; 102 final boolean hidden; 103 final String[] aliases; 104 105 public Option(boolean hasArg, Processing<T> processing, boolean hidden, String... aliases) { 106 this.hasArg = hasArg; 107 this.processing = processing; 108 this.aliases = aliases; 109 this.hidden = hidden; 110 } 111 112 public Option(boolean hasArg, Processing<T> processing, String... aliases) { 113 this(hasArg, processing, false, aliases); 114 } 115 116 public boolean isHidden() { 117 return hidden; 118 } 119 120 public boolean matches(String opt) { 121 for (String a : aliases) { 122 if (a.equals(opt)) { 123 return true; 124 } else if (opt.startsWith("--") 125 && (hasArg && opt.startsWith(a + "="))) { 126 return true; 127 } 128 } 129 return false; 130 } 131 132 public boolean ignoreRest() { 133 return false; 134 } 135 136 void process(T task, String opt, String arg) throws BadArgs { 137 processing.process(task, opt, arg); 138 } 139 } 140 141 private static class PlugOption extends Option<PluginsOptions> { 142 143 public PlugOption(boolean hasArg, 144 Processing<PluginsOptions> processing, boolean hidden, String... aliases) { 145 super(hasArg, processing, hidden, aliases); 146 } 147 148 public PlugOption(boolean hasArg, 149 Processing<PluginsOptions> processing, String... aliases) { 150 super(hasArg, processing, aliases); 151 } 152 } 153 154 private final class PluginsOptions { 155 156 private static final String PLUGINS_PATH = "--plugin-module-path"; 157 private static final String POST_PROCESS = "--post-process-path"; 158 159 private Layer pluginsLayer = Layer.boot(); 160 private String lastSorter; 161 private boolean listPlugins; 162 private Path existingImage; 163 164 // plugin to args maps. Each plugin may be used more than once in command line. 165 // Each such occurrence results in a Map of arguments. So, there could be multiple 166 // args maps per plugin instance. 167 private final Map<Plugin, List<Map<String, String>>> pluginToMaps = new HashMap<>(); 168 private final List<PlugOption> pluginsOptions = new ArrayList<>(); 169 private final List<PlugOption> mainOptions = new ArrayList<>(); 170 171 private PluginsOptions(String pp) throws BadArgs { 172 173 if (pp != null) { 174 String[] dirs = pp.split(File.pathSeparator); 175 List<Path> paths = new ArrayList<>(dirs.length); 176 for (String dir : dirs) { 177 paths.add(Paths.get(dir)); 178 } 179 180 pluginsLayer = createPluginsLayer(paths); 181 } 182 183 Set<String> optionsSeen = new HashSet<>(); 184 for (Plugin plugin : PluginRepository. 185 getPlugins(pluginsLayer)) { 186 if (!Utils.isDisabled(plugin)) { 187 addOrderedPluginOptions(plugin, optionsSeen); 188 } 189 } 190 mainOptions.add(new PlugOption(false, 191 (task, opt, arg) -> { 192 // This option is handled prior 193 // to have the options parsed. 194 }, 195 "--plugins-modulepath")); 196 mainOptions.add(new PlugOption(true, (task, opt, arg) -> { 197 Path path = Paths.get(arg); 198 if (!Files.exists(path) || !Files.isDirectory(path)) { 199 throw newBadArgs("err.existing.image.must.exist"); 200 } 201 existingImage = path.toAbsolutePath(); 202 }, true, POST_PROCESS)); 203 mainOptions.add(new PlugOption(true, 204 (task, opt, arg) -> { 205 lastSorter = arg; 206 }, 207 true, "--resources-last-sorter")); 208 mainOptions.add(new PlugOption(false, 209 (task, opt, arg) -> { 210 listPlugins = true; 211 }, 212 "--list-plugins")); 213 } 214 215 private List<Map<String, String>> argListFor(Plugin plugin) { 216 List<Map<String, String>> mapList = pluginToMaps.get(plugin); 217 if (mapList == null) { 218 mapList = new ArrayList<>(); 219 pluginToMaps.put(plugin, mapList); 220 } 221 return mapList; 222 } 223 224 private void addEmptyArgumentMap(Plugin plugin) { 225 argListFor(plugin).add(Collections.emptyMap()); 226 } 227 228 private Map<String, String> addArgumentMap(Plugin plugin) { 229 Map<String, String> map = new HashMap<>(); 230 argListFor(plugin).add(map); 231 return map; 232 } 233 234 private void addOrderedPluginOptions(Plugin plugin, 235 Set<String> optionsSeen) throws BadArgs { 236 String option = plugin.getOption(); 237 if (option == null) { 238 return; 239 } 240 241 // make sure that more than one plugin does not use the same option! 242 if (optionsSeen.contains(option)) { 243 throw new BadArgs("err.plugin.mutiple.options", 244 option); 245 } 246 optionsSeen.add(option); 247 248 PlugOption plugOption 249 = new PlugOption(plugin.hasArguments(), 250 (task, opt, arg) -> { 251 if (!Utils.isFunctional(plugin)) { 252 throw newBadArgs("err.provider.not.functional", 253 option); 254 } 255 256 if (! plugin.hasArguments()) { 257 addEmptyArgumentMap(plugin); 258 return; 259 } 260 261 Map<String, String> m = addArgumentMap(plugin); 262 // handle one or more arguments 263 if (arg.indexOf(':') == -1) { 264 // single argument case 265 m.put(option, arg); 266 } else { 267 // This option can accept more than one arguments 268 // like --option_name=arg_value:arg2=value2:arg3=value3 269 270 // ":" followed by word char condition takes care of args that 271 // like Windows absolute paths "C:\foo", "C:/foo" [cygwin] etc. 272 // This enforces that key names start with a word character. 273 String[] args = arg.split(":(?=\\w)", -1); 274 String firstArg = args[0]; 275 if (firstArg.isEmpty()) { 276 throw newBadArgs("err.provider.additional.arg.error", 277 option, arg); 278 } 279 m.put(option, firstArg); 280 // process the additional arguments 281 for (int i = 1; i < args.length; i++) { 282 String addArg = args[i]; 283 int eqIdx = addArg.indexOf('='); 284 if (eqIdx == -1) { 285 throw newBadArgs("err.provider.additional.arg.error", 286 option, arg); 287 } 288 289 String addArgName = addArg.substring(0, eqIdx); 290 String addArgValue = addArg.substring(eqIdx+1); 291 if (addArgName.isEmpty() || addArgValue.isEmpty()) { 292 throw newBadArgs("err.provider.additional.arg.error", 293 option, arg); 294 } 295 m.put(addArgName, addArgValue); 296 } 297 } 298 }, 299 "--" + option); 300 pluginsOptions.add(plugOption); 301 302 if (Utils.isFunctional(plugin)) { 303 if (Utils.isAutoEnabled(plugin)) { 304 addEmptyArgumentMap(plugin); 305 } 306 307 if (plugin instanceof DefaultCompressPlugin) { 308 plugOption 309 = new PlugOption(false, 310 (task, opt, arg) -> { 311 Map<String, String> m = addArgumentMap(plugin); 312 m.put(DefaultCompressPlugin.NAME, DefaultCompressPlugin.LEVEL_2); 313 }, "-c"); 314 mainOptions.add(plugOption); 315 } else if (plugin instanceof StripDebugPlugin) { 316 plugOption 317 = new PlugOption(false, 318 (task, opt, arg) -> { 319 addArgumentMap(plugin); 320 }, "-G"); 321 mainOptions.add(plugOption); 322 } 323 } 324 } 325 326 private PlugOption getOption(String name) throws BadArgs { 327 for (PlugOption o : pluginsOptions) { 328 if (o.matches(name)) { 329 return o; 330 } 331 } 332 for (PlugOption o : mainOptions) { 333 if (o.matches(name)) { 334 return o; 335 } 336 } 337 return null; 338 } 339 340 private PluginsConfiguration getPluginsConfig(Path output 341 ) throws IOException, BadArgs { 342 if (output != null) { 343 if (Files.exists(output)) { 344 throw new PluginException(PluginsResourceBundle. 345 getMessage("err.dir.already.exits", output)); 346 } 347 } 348 349 PluginContextImpl pluginContext = new PluginContextImpl(); 350 List<Plugin> pluginsList = new ArrayList<>(); 351 for (Entry<Plugin, List<Map<String, String>>> entry : pluginToMaps.entrySet()) { 352 Plugin plugin = entry.getKey(); 353 List<Map<String, String>> argsMaps = entry.getValue(); 354 355 // same plugin option may be used multiple times in command line. 356 // we call configure once for each occurrence. It is upto the plugin 357 // to 'merge' and/or 'override' arguments. 358 for (Map<String, String> map : argsMaps) { 359 plugin.configure(Collections.unmodifiableMap(map), pluginContext); 360 } 361 362 if (!Utils.isDisabled(plugin)) { 363 pluginsList.add(plugin); 364 } 365 } 366 367 // recreate or postprocessing don't require an output directory. 368 ImageBuilder builder = null; 369 if (output != null) { 370 builder = new DefaultImageBuilder(output); 371 372 } 373 return new Jlink.PluginsConfiguration(pluginsList, 374 builder, lastSorter, pluginContext); 375 } 376 } 377 378 private static final class ResourceBundleHelper { 379 380 private final ResourceBundle bundle; 381 private final ResourceBundle pluginBundle; 382 383 ResourceBundleHelper(String path) { 384 Locale locale = Locale.getDefault(); 385 try { 386 bundle = ResourceBundle.getBundle(path, locale); 387 pluginBundle = ResourceBundle.getBundle("jdk.tools.jlink.resources.plugins", locale); 388 } catch (MissingResourceException e) { 389 throw new InternalError("Cannot find jlink resource bundle for locale " + locale); 390 } 391 } 392 393 String getMessage(String key, Object... args) { 394 String val; 395 try { 396 val = bundle.getString(key); 397 } catch (MissingResourceException e) { 398 // XXX OK, check in plugin bundle 399 val = pluginBundle.getString(key); 400 } 401 return MessageFormat.format(val, args); 402 } 403 404 } 405 406 public final class OptionsHelper<T> { 407 408 private final List<Option<T>> options; 409 private String[] command; 410 private String defaults; 411 412 OptionsHelper(List<Option<T>> options) { 413 this.options = options; 414 } 415 416 private boolean hasArgument(String optionName) throws BadArgs { 417 Option<?> opt = getOption(optionName); 418 if (opt == null) { 419 opt = pluginOptions.getOption(optionName); 420 if (opt == null) { 421 throw new BadArgs("err.unknown.option", optionName). 422 showUsage(true); 423 } 424 } 425 return opt.hasArg; 426 } 427 428 public boolean listPlugins() { 429 return pluginOptions.listPlugins; 430 } 431 432 private String getPluginsPath(String[] args) throws BadArgs { 433 String pp = null; 434 for (int i = 0; i < args.length; i++) { 435 if (args[i].equals(PluginsOptions.PLUGINS_PATH)) { 436 if (i == args.length - 1) { 437 throw new BadArgs("err.no.plugins.path").showUsage(true); 438 } else { 439 i += 1; 440 pp = args[i]; 441 if (!pp.isEmpty() && pp.charAt(0) == '-') { 442 throw new BadArgs("err.no.plugins.path").showUsage(true); 443 } 444 break; 445 } 446 } 447 } 448 return pp; 449 } 450 451 public List<String> handleOptions(T task, String[] args) throws BadArgs { 452 // findbugs warning, copy instead of keeping a reference. 453 command = Arrays.copyOf(args, args.length); 454 455 // Must extract it prior to do any option analysis. 456 // Required to interpret custom plugin options. 457 // Unit tests can call Task multiple time in same JVM. 458 pluginOptions = new PluginsOptions(getPluginsPath(args)); 459 460 // First extract plugins path if any 461 String pp = null; 462 List<String> filteredArgs = new ArrayList<>(); 463 for (int i = 0; i < args.length; i++) { 464 if (args[i].equals(PluginsOptions.PLUGINS_PATH)) { 465 if (i == args.length - 1) { 466 throw new BadArgs("err.no.plugins.path").showUsage(true); 467 } else { 468 warning("warn.thirdparty.plugins.enabled"); 469 log.println(bundleHelper.getMessage("warn.thirdparty.plugins")); 470 i += 1; 471 String arg = args[i]; 472 if (!arg.isEmpty() && arg.charAt(0) == '-') { 473 throw new BadArgs("err.no.plugins.path").showUsage(true); 474 } 475 pp = args[i]; 476 } 477 } else { 478 filteredArgs.add(args[i]); 479 } 480 } 481 String[] arr = new String[filteredArgs.size()]; 482 args = filteredArgs.toArray(arr); 483 484 List<String> rest = new ArrayList<>(); 485 // process options 486 for (int i = 0; i < args.length; i++) { 487 if (!args[i].isEmpty() && args[i].charAt(0) == '-') { 488 String name = args[i]; 489 PlugOption pluginOption = null; 490 Option<T> option = getOption(name); 491 if (option == null) { 492 pluginOption = pluginOptions.getOption(name); 493 if (pluginOption == null) { 494 495 throw new BadArgs("err.unknown.option", name). 496 showUsage(true); 497 } 498 } 499 Option<?> opt = pluginOption == null ? option : pluginOption; 500 String param = null; 501 if (opt.hasArg) { 502 if (name.startsWith("--") && name.indexOf('=') > 0) { 503 param = name.substring(name.indexOf('=') + 1, 504 name.length()); 505 } else if (i + 1 < args.length) { 506 param = args[++i]; 507 } 508 if (param == null || param.isEmpty() 509 || (param.length() >= 2 && param.charAt(0) == '-' 510 && param.charAt(1) == '-')) { 511 throw new BadArgs("err.missing.arg", name). 512 showUsage(true); 513 } 514 } 515 if (pluginOption != null) { 516 pluginOption.process(pluginOptions, name, param); 517 } else { 518 option.process(task, name, param); 519 } 520 if (opt.ignoreRest()) { 521 i = args.length; 522 } 523 } else { 524 rest.add(args[i]); 525 } 526 } 527 return rest; 528 } 529 530 private Option<T> getOption(String name) { 531 for (Option<T> o : options) { 532 if (o.matches(name)) { 533 return o; 534 } 535 } 536 return null; 537 } 538 539 public void showHelp(String progName) { 540 showHelp(progName, true); 541 } 542 543 private void showHelp(String progName, boolean showsImageBuilder) { 544 log.println(bundleHelper.getMessage("main.usage", progName)); 545 for (Option<?> o : options) { 546 String name = o.aliases[0].substring(1); // there must always be at least one name 547 name = name.charAt(0) == '-' ? name.substring(1) : name; 548 if (o.isHidden() || name.equals("h")) { 549 continue; 550 } 551 log.println(bundleHelper.getMessage("main.opt." + name)); 552 } 553 554 for (Option<?> o : pluginOptions.mainOptions) { 555 if (o.aliases[0].equals(PluginsOptions.POST_PROCESS) 556 && !showsImageBuilder) { 557 continue; 558 } 559 String name = o.aliases[0].substring(1); // there must always be at least one name 560 name = name.charAt(0) == '-' ? name.substring(1) : name; 561 if (o.isHidden()) { 562 continue; 563 } 564 log.println(bundleHelper.getMessage("plugin.opt." + name)); 565 } 566 567 log.println(bundleHelper.getMessage("main.command.files")); 568 } 569 570 public void listPlugins(boolean showsImageBuilder) { 571 log.println("\n" + bundleHelper.getMessage("main.extended.help")); 572 List<Plugin> pluginList = PluginRepository. 573 getPlugins(pluginOptions.pluginsLayer); 574 for (Plugin plugin : Utils. 575 getSortedPreProcessors(pluginList)) { 576 showPlugin(plugin, log, showsImageBuilder); 577 } 578 579 if (showsImageBuilder) { 580 for (Plugin plugin : Utils.getSortedPostProcessors(pluginList)) { 581 showPlugin(plugin, log, showsImageBuilder); 582 } 583 } 584 } 585 586 private void showPlugin(Plugin plugin, PrintWriter log, boolean showsImageBuilder) { 587 if (showsPlugin(plugin, showsImageBuilder)) { 588 log.println("\n" + bundleHelper.getMessage("main.plugin.name") 589 + ": " + plugin.getName()); 590 591 // print verbose details for non-builtin plugins 592 if (!Utils.isBuiltin(plugin)) { 593 log.println(bundleHelper.getMessage("main.plugin.class") 594 + ": " + plugin.getClass().getName()); 595 log.println(bundleHelper.getMessage("main.plugin.module") 596 + ": " + plugin.getClass().getModule().getName()); 597 CATEGORY category = Utils.getCategory(plugin); 598 log.println(bundleHelper.getMessage("main.plugin.category") 599 + ": " + category.getName()); 600 log.println(bundleHelper.getMessage("main.plugin.state") 601 + ": " + plugin.getStateDescription()); 602 } 603 604 String option = plugin.getOption(); 605 if (option != null) { 606 log.println(bundleHelper.getMessage("main.plugin.option") 607 + ": --" + plugin.getOption() 608 + (plugin.hasArguments()? ("=" + plugin.getArgumentsDescription()) : "")); 609 } 610 611 // description can be long spanning more than one line and so 612 // print a newline after description label. 613 log.println(bundleHelper.getMessage("main.plugin.description") 614 + ": " + plugin.getDescription()); 615 } 616 } 617 618 String[] getInputCommand() { 619 return command; 620 } 621 622 String getDefaults() { 623 return defaults; 624 } 625 626 public Layer getPluginsLayer() { 627 return pluginOptions.pluginsLayer; 628 } 629 } 630 631 private PluginsOptions pluginOptions; 632 private PrintWriter log; 633 private final ResourceBundleHelper bundleHelper; 634 635 public TaskHelper(String path) { 636 if (!JLINK_BUNDLE.equals(path) && !JIMAGE_BUNDLE.equals(path)) { 637 throw new IllegalArgumentException("Invalid Bundle"); 638 } 639 this.bundleHelper = new ResourceBundleHelper(path); 640 } 641 642 public <T> OptionsHelper<T> newOptionsHelper(Class<T> clazz, 643 Option<?>[] options) { 644 List<Option<T>> optionsList = new ArrayList<>(); 645 for (Option<?> o : options) { 646 @SuppressWarnings("unchecked") 647 Option<T> opt = (Option<T>) o; 648 optionsList.add(opt); 649 } 650 return new OptionsHelper<>(optionsList); 651 } 652 653 public BadArgs newBadArgs(String key, Object... args) { 654 return new BadArgs(key, args); 655 } 656 657 public String getMessage(String key, Object... args) { 658 return bundleHelper.getMessage(key, args); 659 } 660 661 public void setLog(PrintWriter log) { 662 this.log = log; 663 } 664 665 public void reportError(String key, Object... args) { 666 log.println(bundleHelper.getMessage("error.prefix") + " " 667 + bundleHelper.getMessage(key, args)); 668 } 669 670 public void reportUnknownError(String message) { 671 log.println(bundleHelper.getMessage("error.prefix") + " " + message); 672 } 673 674 public void warning(String key, Object... args) { 675 log.println(bundleHelper.getMessage("warn.prefix") + " " 676 + bundleHelper.getMessage(key, args)); 677 } 678 679 public PluginsConfiguration getPluginsConfig(Path output) 680 throws IOException, BadArgs { 681 return pluginOptions.getPluginsConfig(output); 682 } 683 684 public Path getExistingImage() { 685 return pluginOptions.existingImage; 686 } 687 688 public void showVersion(boolean full) { 689 log.println(version(full ? "full" : "release")); 690 } 691 692 public String version(String key) { 693 return System.getProperty("java.version"); 694 } 695 696 static Layer createPluginsLayer(List<Path> paths) { 697 Path[] arr = new Path[paths.size()]; 698 paths.toArray(arr); 699 ModuleFinder finder = ModuleFinder.of(arr); 700 701 // jmods are located at link-time 702 if (finder instanceof ConfigurableModuleFinder) { 703 ((ConfigurableModuleFinder) finder).configurePhase(Phase.LINK_TIME); 704 } 705 706 Configuration bootConfiguration = Layer.boot().configuration(); 707 try { 708 Configuration cf = bootConfiguration 709 .resolveRequiresAndUses(ModuleFinder.empty(), 710 finder, 711 Collections.emptySet()); 712 ClassLoader scl = ClassLoader.getSystemClassLoader(); 713 return Layer.boot().defineModulesWithOneLoader(cf, scl); 714 } catch (Exception ex) { 715 // Malformed plugin modules (e.g.: same package in multiple modules). 716 throw new PluginException("Invalid modules in the plugins path: " + ex); 717 } 718 } 719 720 // Display all plugins or pre processors only. 721 private static boolean showsPlugin(Plugin plugin, boolean showsImageBuilder) { 722 if (!Utils.isDisabled(plugin) && plugin.getOption() != null) { 723 if (Utils.isPostProcessor(plugin) && !showsImageBuilder) { 724 return false; 725 } 726 return true; 727 } 728 return false; 729 } 730 }