1 /*
   2  * Copyright (c) 2011, 2015, 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 package com.sun.javafx.tools.packager;
  27 
  28 import com.oracle.tools.packager.*;
  29 import com.oracle.tools.packager.RelativeFileSet;
  30 import com.oracle.tools.packager.jnlp.JNLPBundler;
  31 import com.sun.javafx.tools.ant.Callback;
  32 import com.sun.javafx.tools.packager.bundlers.*;
  33 import com.sun.javafx.tools.packager.bundlers.Bundler.BundleType;
  34 import com.sun.javafx.tools.resource.DeployResource;
  35 import java.io.File;
  36 import java.io.IOException;
  37 import java.util.ArrayList;
  38 import java.util.Arrays;
  39 import java.util.Collection;
  40 import java.util.HashMap;
  41 import java.util.LinkedHashMap;
  42 import java.util.LinkedHashSet;
  43 import java.util.LinkedList;
  44 import java.util.List;
  45 import java.util.Map;
  46 import java.util.Set;
  47 import java.util.TreeMap;
  48 import java.util.TreeSet;
  49 import java.util.stream.Collectors;
  50 
  51 import static com.oracle.tools.packager.jnlp.JNLPBundler.*;
  52 
  53 public class DeployParams extends CommonParams {
  54     public enum RunMode {
  55         WEBSTART, EMBEDDED, STANDALONE, ALL
  56     }
  57 
  58     final List<RelativeFileSet> resources = new ArrayList<>();
  59 
  60     String id;
  61     String title;
  62     String vendor;
  63     String email;
  64     String description;
  65     String category;
  66     String licenseType;
  67     String copyright;
  68     String version;
  69     Boolean systemWide;
  70     Boolean serviceHint;
  71     Boolean signBundle;
  72     Boolean installdirChooser;
  73 
  74     String applicationClass;
  75     String preloader;
  76 
  77     List<Param> params;
  78     List<HtmlParam> htmlParams;
  79     List<String> arguments; //unnamed arguments
  80 
  81     int width;
  82     int height;
  83     String embeddedWidth = null;
  84     String embeddedHeight = null;
  85 
  86     String appName;
  87     String codebase;
  88 
  89     boolean embedJNLP = true;
  90     @Deprecated final boolean embedCertificates = false;
  91     boolean allPermissions = false;
  92     String updateMode = "background";
  93     boolean isExtension = false;
  94     boolean isSwingApp = false;
  95 
  96     Boolean needShortcut = null;
  97     Boolean needMenu = null;
  98     boolean needInstall = false;
  99 
 100     String outfile;
 101     //if true then we cobundle js and image files needed
 102     // for web deployment with the application
 103     boolean includeDT;
 104 
 105     String placeholder = "'javafx-app-placeholder'";
 106     String appId = null;
 107 
 108     // didn't have a setter...
 109     boolean offlineAllowed = true;
 110 
 111     List<JSCallback> callbacks;
 112 
 113     //list of HTML templates to process
 114     List<Template> templates = new LinkedList<>();
 115 
 116     String jrePlatform = "1.6+";
 117     String fxPlatform = PackagerLib.JAVAFX_VERSION+"+";
 118     File javaRuntimeToUse = null;
 119     boolean javaRuntimeWasSet = false;
 120 
 121     //list of jvm args (in theory string can contain spaces and need to be escaped
 122     List<String> jvmargs = new LinkedList<>();
 123     Map<String, String> jvmUserArgs = new LinkedHashMap<>();
 124 
 125     //list of jvm properties (can also be passed as VM args
 126     // but keeping them separate make it a bit more convinient for JNLP generation)
 127     Map<String, String> properties = new LinkedHashMap<>();
 128     
 129     // raw arguments to the bundler
 130     Map<String, ? super Object> bundlerArguments = new LinkedHashMap<>();
 131 
 132     String fallbackApp = null;
 133 
 134     public void setJavaRuntimeSource(File src) {
 135         javaRuntimeToUse = src;
 136         javaRuntimeWasSet = true;
 137     }
 138 
 139     public void setCodebase(String codebase) {
 140         this.codebase = codebase;
 141     }
 142 
 143     public void setId(String id) {
 144         this.id = id;
 145     }
 146 
 147     public void setCategory(String category) {
 148         this.category = category;
 149     }
 150 
 151     public void setLicenseType(String licenseType) {
 152         this.licenseType = licenseType;
 153     }
 154 
 155     public void setCopyright(String copyright) {
 156         this.copyright = copyright;
 157     }
 158 
 159     public void setVersion(String version) {
 160         this.version = version;
 161     }
 162 
 163     public void setSystemWide(Boolean systemWide) {
 164         this.systemWide = systemWide;
 165     }
 166 
 167     public void setServiceHint(Boolean serviceHint) {
 168         this.serviceHint = serviceHint;
 169     }
 170 
 171     public void setInstalldirChooser(Boolean installdirChooser) {
 172         this.installdirChooser = installdirChooser;
 173     }
 174     
 175     public void setSignBundle(Boolean signBundle) {
 176         this.signBundle = signBundle;
 177     }
 178 
 179     public void setJRE(String v) {
 180         jrePlatform = v;
 181     }
 182 
 183     public void setSwingAppWithEmbeddedJavaFX(boolean v) {
 184         isSwingApp = v;
 185     }
 186 
 187     public void setNeedInstall(boolean b) {
 188         needInstall = b;
 189     }
 190 
 191     public void setOfflineAllowed(boolean b) {
 192         offlineAllowed = b;
 193     }
 194 
 195     public void setNeedShortcut(Boolean b) {
 196         needShortcut = b;
 197     }
 198 
 199     public void setNeedMenu(Boolean b) {
 200         needMenu = b;
 201     }
 202 
 203     public void setEmbeddedDimensions(String w, String h) {
 204         embeddedWidth = w;
 205         embeddedHeight = h;
 206     }
 207 
 208     public void setFallback(String v) {
 209         if (v == null) {
 210             return;
 211         }
 212 
 213         if ("none".equals(v) || "null".equals(v)) {
 214             fallbackApp = null;
 215         } else {
 216             fallbackApp = v;
 217         }
 218     }
 219 
 220     public void setJavafx(String v) {
 221         fxPlatform = v;
 222     }
 223 
 224     public void addJvmArg(String v) {
 225         jvmargs.add(v);
 226     }
 227 
 228     public void addJvmUserArg(String n, String v) {
 229         jvmUserArgs.put(n, v);
 230     }
 231 
 232     public void addJvmProperty(String n, String v) {
 233         properties.put(n, v);
 234     }
 235 
 236     public void setAllPermissions(boolean allPermissions) {
 237         this.allPermissions = allPermissions;
 238     }
 239 
 240     public void setAppName(String appName) {
 241         this.appName = appName;
 242     }
 243 
 244     public void setArguments(List<String> args) {
 245         this.arguments = args;
 246     }
 247 
 248     public void setDescription(String description) {
 249         this.description = description;
 250     }
 251 
 252     public void setEmbedJNLP(boolean embedJNLP) {
 253         this.embedJNLP = embedJNLP;
 254     }
 255 
 256     @Deprecated
 257     public void setEmbedCertifcates(boolean v) {
 258         if (v) {
 259             System.out.println("JavaFX Packager no longer supports embedding certificates in JNLP files.  Setting will be ignored.");
 260         }
 261     }
 262 
 263     public void setPlaceholder(String p) {
 264         placeholder = p;
 265     }
 266 
 267     public void setAppId(String id) {
 268         appId = id;
 269     }
 270 
 271     public void setHeight(int height) {
 272         this.height = height;
 273     }
 274 
 275     public void setHtmlParams(List<HtmlParam> htmlParams) {
 276         this.htmlParams = htmlParams;
 277     }
 278 
 279     public void setOutfile(String outfile) {
 280         this.outfile = outfile;
 281     }
 282 
 283     public void setParams(List<Param> params) {
 284         this.params = params;
 285     }
 286 
 287     public void setPreloader(String preloader) {
 288         this.preloader = preloader;
 289     }
 290 
 291     public void setTitle(String title) {
 292         this.title = title;
 293     }
 294 
 295     public void setUpdateMode(String updateMode) {
 296         this.updateMode = updateMode;
 297     }
 298 
 299     public void setVendor(String vendor) {
 300         this.vendor = vendor;
 301     }
 302 
 303     public void setEmail(String email) {
 304         this.email = email;
 305     }
 306 
 307     public void setWidth(int width) {
 308         this.width = width;
 309     }
 310 
 311     public void setExtension(boolean isExtension) {
 312         this.isExtension = isExtension;
 313     }
 314 
 315     public void setApplicationClass(String applicationClass) {
 316         this.applicationClass = applicationClass;
 317     }
 318 
 319     public void setIncludeDT(boolean doEmbed) {
 320         includeDT = doEmbed;
 321     }
 322 
 323     public void setJSCallbacks(List<JSCallback> list) {
 324         callbacks = list;
 325     }
 326 
 327     public void setCallbacks(List<Callback> list) {
 328         List<JSCallback> jslist = new ArrayList<>(list.size());
 329         for (Callback cb: list) {
 330             jslist.add(new JSCallback(cb.getName(), cb.getCmd()));
 331         }
 332         callbacks = jslist;
 333     }
 334 
 335     static class Template {
 336         File in;
 337         File out;
 338 
 339         Template(File in, File out) {
 340             this.in = in;
 341             this.out = out;
 342         }
 343     }
 344 
 345     public void addTemplate(File in, File out) {
 346         templates.add(new Template(in, out));
 347     }
 348 
 349     //we need to expand as in some cases
 350     // (most notably javapackager)
 351     //we may get "." as filename and assumption is we include
 352     // everything in the given folder
 353     // (IOUtils.copyfiles() have recursive behavior)
 354     List<File> expandFileset(File root) {
 355         List<File> files = new LinkedList<>();
 356         if (com.oracle.tools.packager.IOUtils.isNotSymbolicLink(root)) {
 357             if (root.isDirectory()) {
 358                 File[] children = root.listFiles();
 359                 if (children != null) {
 360                     for (File f : children) {
 361                         files.addAll(expandFileset(f));
 362                     }
 363                 }
 364             } else {
 365                 files.add(root);
 366             }
 367         }
 368         return files;
 369     }
 370 
 371     @Override
 372     public void addResource(File baseDir, String path) {
 373         File file = new File(baseDir, path);
 374         //normalize top level dir
 375         // to strip things like "." in the path
 376         // or it can confuse symlink detection logic
 377         file = file.getAbsoluteFile();
 378 
 379         if (baseDir == null) {
 380             baseDir = file.getParentFile();
 381         }
 382         resources.add(new RelativeFileSet(baseDir, new LinkedHashSet<>(expandFileset(file))));
 383     }
 384 
 385     @Override
 386     public void addResource(File baseDir, File file) {
 387         //normalize initial file
 388         // to strip things like "." in the path
 389         // or it can confuse symlink detection logic
 390         file = file.getAbsoluteFile();
 391 
 392         if (baseDir == null) {
 393             baseDir = file.getParentFile();
 394         }
 395         resources.add(new RelativeFileSet(baseDir, new LinkedHashSet<>(expandFileset(file))));
 396     }
 397 
 398     public void addResource(File baseDir, String path, String type) {
 399         addResource(baseDir, createFile(baseDir, path), type);
 400     }
 401 
 402     public void addResource(File baseDir, File file, String type) {
 403         addResource(baseDir, file, "eager", type, null, null);
 404     }
 405 
 406     public void addResource(File baseDir, File file, String mode, String type, String os, String arch) {
 407         Set<File> singleFile = new LinkedHashSet<>();
 408         singleFile.add(file);
 409         if (baseDir == null) {
 410             baseDir = file.getParentFile();
 411         }
 412         RelativeFileSet rfs = new RelativeFileSet(baseDir, singleFile);
 413         rfs.setArch(arch);
 414         rfs.setMode(mode);
 415         rfs.setOs(os);
 416         rfs.setType(parseTypeFromString(type, file));
 417         resources.add(rfs);
 418     }
 419 
 420     private RelativeFileSet.Type parseTypeFromString(String type, File file) {
 421         if (type == null) {
 422             if (file.getName().endsWith(".jar")) {
 423                 return RelativeFileSet.Type.jar;
 424             } else if (file.getName().endsWith(".jnlp")) {
 425                 return RelativeFileSet.Type.jnlp;
 426             } else {
 427                 return RelativeFileSet.Type.UNKNOWN;
 428             }
 429         } else {
 430             return RelativeFileSet.Type.valueOf(type);
 431         }
 432     }
 433     
 434     private static File createFile(final File baseDir, final String path) {
 435         final File testFile = new File(path);
 436         return testFile.isAbsolute()
 437                 ? testFile
 438                 : new File(baseDir == null 
 439                     ? null
 440                     : baseDir.getAbsolutePath(),
 441                       path);
 442     }
 443 
 444 
 445     @Override
 446     public void validate() throws PackagerException {
 447         if (outdir == null) {
 448             throw new PackagerException("ERR_MissingArgument", "-outdir");
 449         }
 450         if (outfile == null) {
 451             throw new PackagerException("ERR_MissingArgument", "-outfile");
 452         }
 453         if (resources.isEmpty()) {
 454             throw new PackagerException("ERR_MissingAppResources");
 455         }
 456         if (applicationClass == null) {
 457             throw new PackagerException("ERR_MissingArgument", "-appclass");
 458         }
 459     }
 460 
 461     //could be icon or splash
 462     static class Icon {
 463         final static int UNDEFINED = -1;
 464 
 465         String href;
 466         String kind;
 467         int width = UNDEFINED;
 468         int height = UNDEFINED;
 469         int depth = UNDEFINED;
 470         RunMode mode = RunMode.WEBSTART;
 471 
 472         Icon(String href, String kind, int w, int h, int d, RunMode m) {
 473             mode = m;
 474             this.href = href;
 475             this.kind = kind;
 476             if (w > 0) {
 477                 width = w;
 478             }
 479             if (h > 0) {
 480                 height = h;
 481             }
 482             if (d > 0) {
 483                 depth = d;
 484             }
 485         }
 486     }
 487 
 488     List<Icon> icons = new LinkedList<>();
 489 
 490     public void addIcon(String href, String kind, int w, int h, int d, RunMode m) {
 491         icons.add(new Icon(href, kind, w, h, d, m));
 492     }
 493 
 494     BundleType bundleType = BundleType.NONE;
 495     String targetFormat = null; //means any
 496 
 497     public void setBundleType(BundleType type) {
 498         bundleType = type;
 499     }
 500     
 501     public BundleType getBundleType() {
 502         return bundleType;
 503     }
 504 
 505     public void setTargetFormat(String t) {
 506         targetFormat = t;
 507     }
 508 
 509     public String getTargetFormat() {
 510         return targetFormat;
 511     }
 512 
 513     private String getArch() {
 514         String arch = System.getProperty("os.arch").toLowerCase();
 515 
 516         if ("x86".equals(arch) || "i386".equals(arch) || "i486".equals(arch)
 517                 || "i586".equals(arch) || "i686".equals(arch)) {
 518             arch = "x86";
 519         } else if ("x86_64".equals(arch) || "amd64".equals("arch")) {
 520             arch = "x86_64";
 521         }
 522 
 523         return arch;
 524     }
 525 
 526     static final Set<String> multi_args = new TreeSet<>(Arrays.asList(
 527             StandardBundlerParam.JVM_PROPERTIES.getID(),
 528             StandardBundlerParam.JVM_OPTIONS.getID(),
 529             StandardBundlerParam.USER_JVM_OPTIONS.getID(),
 530             StandardBundlerParam.ARGUMENTS.getID()
 531     ));
 532 
 533     @SuppressWarnings("unchecked")
 534     public void addBundleArgument(String key, Object value) {
 535         // special hack for multi-line arguments
 536         if (multi_args.contains(key) && value instanceof String) {
 537             Object existingValue = bundlerArguments.get(key);
 538             if (existingValue instanceof String) {
 539                 bundlerArguments.put(key, existingValue + "\n\n" + value);
 540             } else if (existingValue instanceof List) {
 541                 ((List)existingValue).add(value);
 542             } else if (existingValue instanceof Map && ((String)value).contains("=")) {
 543                 String[] mapValues = ((String)value).split("=", 2);
 544                 ((Map)existingValue).put(mapValues[0], mapValues[1]);
 545             } else {
 546                 bundlerArguments.put(key, value);
 547             }
 548         } else {
 549             bundlerArguments.put(key, value);
 550         }
 551     }
 552 
 553     public BundleParams getBundleParams() {
 554         BundleParams bundleParams = new BundleParams();
 555 
 556         //construct app resources
 557         //  relative to output folder!
 558         String currentOS = System.getProperty("os.name").toLowerCase();
 559         String currentArch = getArch();
 560 
 561         for (RelativeFileSet rfs : resources) {
 562             String os = rfs.getOs();
 563             String arch = rfs.getArch();
 564             //skip resources for other OS
 565             // and nativelib jars (we are including raw libraries)
 566             if ((os == null || currentOS.contains(os.toLowerCase())) &&
 567                     (arch == null || currentArch.startsWith(arch.toLowerCase()))
 568                     && rfs.getType() != RelativeFileSet.Type.nativelib) {
 569                 if (rfs.getType() == RelativeFileSet.Type.license) {
 570                     for (String s : rfs.getIncludedFiles()) {
 571                         bundleParams.addLicenseFile(s);
 572                     }
 573                 }
 574             }
 575         }
 576         
 577         bundleParams.setAppResourcesList(resources);
 578 
 579         bundleParams.setIdentifier(id);
 580 
 581         if (javaRuntimeWasSet) {
 582             bundleParams.setRuntime(javaRuntimeToUse);
 583         }
 584         bundleParams.setApplicationClass(applicationClass);
 585         bundleParams.setPrelaoderClass(preloader);
 586         bundleParams.setName(this.appName);
 587         bundleParams.setAppVersion(version);
 588         bundleParams.setType(bundleType);
 589         bundleParams.setBundleFormat(targetFormat);
 590         bundleParams.setVendor(vendor);
 591         bundleParams.setEmail(email);
 592         bundleParams.setShortcutHint(needShortcut);
 593         bundleParams.setMenuHint(needMenu);
 594         bundleParams.setSystemWide(systemWide);
 595         bundleParams.setServiceHint(serviceHint);
 596         bundleParams.setInstalldirChooser(installdirChooser);
 597         bundleParams.setSignBundle(signBundle);
 598         bundleParams.setCopyright(copyright);
 599         bundleParams.setApplicationCategory(category);
 600         bundleParams.setLicenseType(licenseType);
 601         bundleParams.setDescription(description);
 602         bundleParams.setTitle(title);
 603         if (verbose) bundleParams.setVerbose(true);
 604 
 605         bundleParams.setJvmProperties(properties);
 606         bundleParams.setJvmargs(jvmargs);
 607         bundleParams.setJvmUserArgs(jvmUserArgs);
 608         bundleParams.setArguments(arguments);
 609 
 610         File appIcon = null;
 611         for (Icon ic: icons) {
 612             //NB: in theory we should be paying attention to RunMode but
 613             // currently everything is marked as webstart internally and runmode
 614             // is not publicly documented property
 615             if (/* (ic.mode == RunMode.ALL || ic.mode == RunMode.STANDALONE) && */
 616                 (ic.kind == null || ic.kind.equals("default"))) 
 617             {
 618                 //could be full path or something relative to the output folder
 619                 appIcon = new File(ic.href);
 620                 if (!appIcon.exists()) {
 621                     com.oracle.tools.packager.Log.debug("Icon [" + ic.href + "] is not valid absolute path. " +
 622                             "Assume it is relative to the output dir.");
 623                     appIcon = new File(outdir, ic.href);
 624                 }
 625             }
 626         }
 627 
 628         bundleParams.setIcon(appIcon);
 629 
 630         Map<String, String> paramsMap = new TreeMap<>();
 631         if (params != null) {
 632             for (Param p : params) {
 633                 paramsMap.put(p.name, p.value);
 634             }
 635         }
 636         putUnlessNullOrEmpty(JNLPBundler.APP_PARAMS.getID(), paramsMap);
 637         
 638         Map<String, String> unescapedHtmlParams = new TreeMap<>();
 639         Map<String, String> escapedHtmlParams = new TreeMap<>();
 640         if (htmlParams != null) {
 641             for (HtmlParam hp : htmlParams) {
 642                 if (hp.needEscape) {
 643                     escapedHtmlParams.put(hp.name, hp.value);
 644                 } else {
 645                     unescapedHtmlParams.put(hp.name, hp.value);
 646                 }
 647             }
 648         }
 649         putUnlessNullOrEmpty(JNLPBundler.APPLET_PARAMS.getID(), unescapedHtmlParams);
 650         putUnlessNullOrEmpty(ESCAPED_APPLET_PARAMS.getID(), escapedHtmlParams);
 651         
 652 
 653         putUnlessNull(WIDTH.getID(), width);
 654         putUnlessNull(HEIGHT.getID(), height);
 655         putUnlessNull(EMBEDDED_WIDTH.getID(), embeddedWidth);
 656         putUnlessNull(EMBEDDED_HEIGHT.getID(), embeddedHeight);
 657         
 658         putUnlessNull(CODEBASE.getID(), codebase);
 659         putUnlessNull(EMBED_JNLP.getID(), embedJNLP);
 660         // embedCertificates
 661         putUnlessNull(ALL_PERMISSIONS.getID(), allPermissions);
 662         putUnlessNull(UPDATE_MODE.getID(), updateMode);
 663         putUnlessNull(EXTENSION.getID(), isExtension);
 664         putUnlessNull(SWING_APP.getID(), isSwingApp);
 665 
 666         putUnlessNull(OUT_FILE.getID(), outfile);
 667         putUnlessNull(INCLUDE_DT.getID(), includeDT);
 668         putUnlessNull(PLACEHOLDER.getID(), placeholder);
 669         putUnlessNull(OFFLINE_ALLOWED.getID(), offlineAllowed);
 670         
 671         Map<String, String> callbacksMap = new TreeMap<>();
 672         if (callbacks != null) {
 673             for (JSCallback callback : callbacks) {
 674                 callbacksMap.put(callback.getName(), callback.getCmd());
 675             }
 676         }
 677         putUnlessNull(JS_CALLBACKS.getID(), callbacksMap);
 678 
 679         Map<File, File> templatesMap = new TreeMap<>();
 680         if (templates != null) {
 681             for (Template template : templates) {
 682                 templatesMap.put(template.in, template.out);
 683             }
 684         }
 685         putUnlessNull(TEMPLATES.getID(), templatesMap);
 686 
 687         putUnlessNull(FX_PLATFORM.getID(), fxPlatform);
 688         putUnlessNull(JRE_PLATFORM.getID(), jrePlatform);
 689 
 690         putUnlessNull(FALLBACK_APP.getID(), fallbackApp);
 691         
 692         // check for collisions
 693         TreeSet<String> keys = new TreeSet<>(bundlerArguments.keySet());
 694         keys.retainAll(bundleParams.getBundleParamsAsMap().keySet());
 695 
 696         if (!keys.isEmpty()) {
 697             throw new RuntimeException("Deploy Params and Bundler Arguments overlap in the following values:" + keys.toString());
 698         }
 699         
 700         bundleParams.addAllBundleParams(bundlerArguments);
 701         
 702         return bundleParams;
 703     }
 704 
 705     public void putUnlessNull(String param, Object value) {
 706         if (value != null) {
 707             bundlerArguments.put(param, value);
 708         }
 709     }
 710 
 711     public void putUnlessNullOrEmpty(String param, Map value) {
 712         if (value != null && !value.isEmpty()) {
 713             bundlerArguments.put(param, value);
 714         }
 715     }
 716 }