001/* 002 * (C) Copyright 2006-2010 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Thierry Delprat 018 */ 019package org.nuxeo.apidoc.browse; 020 021import java.io.IOException; 022import java.io.Reader; 023import java.io.StringReader; 024import java.io.StringWriter; 025import java.net.URI; 026import java.net.URISyntaxException; 027import java.util.ArrayList; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.stream.Collectors; 033 034import javax.ws.rs.FormParam; 035import javax.ws.rs.GET; 036import javax.ws.rs.POST; 037import javax.ws.rs.Path; 038import javax.ws.rs.PathParam; 039import javax.ws.rs.Produces; 040import javax.ws.rs.QueryParam; 041import javax.ws.rs.core.Response; 042 043import org.json.JSONArray; 044import org.json.JSONException; 045import org.json.JSONObject; 046import org.nuxeo.apidoc.api.BundleGroup; 047import org.nuxeo.apidoc.api.BundleGroupFlatTree; 048import org.nuxeo.apidoc.api.BundleGroupTreeHelper; 049import org.nuxeo.apidoc.api.BundleInfo; 050import org.nuxeo.apidoc.api.ComponentInfo; 051import org.nuxeo.apidoc.api.DocumentationItem; 052import org.nuxeo.apidoc.api.ExtensionInfo; 053import org.nuxeo.apidoc.api.ExtensionPointInfo; 054import org.nuxeo.apidoc.api.NuxeoArtifact; 055import org.nuxeo.apidoc.api.OperationInfo; 056import org.nuxeo.apidoc.api.SeamComponentInfo; 057import org.nuxeo.apidoc.api.ServiceInfo; 058import org.nuxeo.apidoc.documentation.DocumentationService; 059import org.nuxeo.apidoc.search.ArtifactSearcher; 060import org.nuxeo.apidoc.snapshot.DistributionSnapshot; 061import org.nuxeo.apidoc.snapshot.SnapshotManager; 062import org.nuxeo.apidoc.tree.TreeHelper; 063import org.nuxeo.ecm.automation.OperationException; 064import org.nuxeo.ecm.core.api.NuxeoException; 065import org.nuxeo.ecm.platform.rendering.wiki.WikiSerializer; 066import org.nuxeo.ecm.webengine.model.Resource; 067import org.nuxeo.ecm.webengine.model.WebObject; 068import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException; 069import org.nuxeo.ecm.webengine.model.impl.DefaultObject; 070import org.nuxeo.runtime.api.Framework; 071import org.wikimodel.wem.WikiParserException; 072 073@WebObject(type = "apibrowser") 074public class ApiBrowser extends DefaultObject { 075 076 protected String distributionId; 077 078 protected boolean embeddedMode = false; 079 080 protected SnapshotManager getSnapshotManager() { 081 return Framework.getLocalService(SnapshotManager.class); 082 } 083 084 protected ArtifactSearcher getSearcher() { 085 return Framework.getLocalService(ArtifactSearcher.class); 086 } 087 088 @Override 089 protected void initialize(Object... args) { 090 distributionId = (String) args[0]; 091 if (args.length > 1) { 092 Boolean embed = (Boolean) args[1]; 093 embeddedMode = embed != null && embed; 094 } 095 } 096 097 @GET 098 @Produces("text/plain") 099 @Path("tree") 100 public Object tree(@QueryParam("root") String source) { 101 return TreeHelper.updateTree(getContext(), source); 102 } 103 104 @GET 105 @Produces("text/html") 106 @Path("treeView") 107 public Object treeView() { 108 return getView("tree").arg(Distribution.DIST_ID, ctx.getProperty(Distribution.DIST_ID)); 109 } 110 111 @GET 112 @Produces("text/html") 113 public Object doGet() { 114 if (embeddedMode) { 115 DistributionSnapshot snap = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()); 116 Map<String, Integer> stats = new HashMap<String, Integer>(); 117 stats.put("bundles", snap.getBundleIds().size()); 118 stats.put("jComponents", snap.getJavaComponentIds().size()); 119 stats.put("xComponents", snap.getXmlComponentIds().size()); 120 stats.put("services", snap.getServiceIds().size()); 121 stats.put("xps", snap.getExtensionPointIds().size()); 122 stats.put("contribs", snap.getComponentIds().size()); 123 return getView("indexSimple").arg(Distribution.DIST_ID, ctx.getProperty(Distribution.DIST_ID)).arg("stats", 124 stats); 125 } else { 126 return getView("index").arg(Distribution.DIST_ID, ctx.getProperty(Distribution.DIST_ID)); 127 } 128 } 129 130 @GET 131 @Produces("text/html") 132 @Path("listBundleGroups") 133 public Object getMavenGroups() { 134 BundleGroupTreeHelper bgth = new BundleGroupTreeHelper(getSnapshotManager().getSnapshot(distributionId, 135 ctx.getCoreSession())); 136 List<BundleGroupFlatTree> tree = bgth.getBundleGroupTree(); 137 return getView("listBundleGroups").arg("tree", tree).arg(Distribution.DIST_ID, 138 ctx.getProperty(Distribution.DIST_ID)); 139 } 140 141 public Map<String, DocumentationItem> getDescriptions(String targetType) { 142 DocumentationService ds = Framework.getLocalService(DocumentationService.class); 143 return ds.getAvailableDescriptions(getContext().getCoreSession(), targetType); 144 } 145 146 @GET 147 @Produces("text/html") 148 @Path("listBundles") 149 public Object getBundles() { 150 List<String> bundleIds = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()).getBundleIds(); 151 return getView("listBundles").arg("bundleIds", bundleIds).arg(Distribution.DIST_ID, 152 ctx.getProperty(Distribution.DIST_ID)); 153 } 154 155 @GET 156 @Produces("text/html") 157 @Path("listComponents") 158 public Object getComponents() { 159 List<String> javaComponentIds = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()).getJavaComponentIds(); 160 List<ArtifactLabel> javaLabels = new ArrayList<ArtifactLabel>(); 161 for (String id : javaComponentIds) { 162 javaLabels.add(ArtifactLabel.createLabelFromComponent(id)); 163 } 164 165 List<String> xmlComponentIds = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()).getXmlComponentIds(); 166 List<ArtifactLabel> xmlLabels = new ArrayList<ArtifactLabel>(); 167 for (String id : xmlComponentIds) { 168 xmlLabels.add(ArtifactLabel.createLabelFromComponent(id)); 169 } 170 171 Collections.sort(javaLabels); 172 Collections.sort(xmlLabels); 173 174 return getView("listComponents").arg("javaComponents", javaLabels).arg("xmlComponents", xmlLabels).arg( 175 Distribution.DIST_ID, ctx.getProperty(Distribution.DIST_ID)); 176 } 177 178 @GET 179 @Produces("text/html") 180 @Path("listServices") 181 public Object getServices() { 182 List<String> serviceIds = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()).getServiceIds(); 183 184 List<ArtifactLabel> serviceLabels = new ArrayList<ArtifactLabel>(); 185 186 for (String id : serviceIds) { 187 serviceLabels.add(ArtifactLabel.createLabelFromService(id)); 188 } 189 Collections.sort(serviceLabels); 190 191 return getView("listServices").arg("services", serviceLabels).arg(Distribution.DIST_ID, 192 ctx.getProperty(Distribution.DIST_ID)); 193 } 194 195 protected Map<String, String> getRenderedDescriptions(String type) { 196 197 Map<String, DocumentationItem> descs = getDescriptions(type); 198 Map<String, String> result = new HashMap<String, String>(); 199 200 for (String key : descs.keySet()) { 201 DocumentationItem docItem = descs.get(key); 202 String content = docItem.getContent(); 203 if ("wiki".equals(docItem.getRenderingType())) { 204 Reader reader = new StringReader(content); 205 WikiSerializer engine = new WikiSerializer(); 206 StringWriter writer = new StringWriter(); 207 try { 208 engine.serialize(reader, writer); 209 } catch (IOException | WikiParserException e) { 210 throw new NuxeoException(e); 211 } 212 content = writer.getBuffer().toString(); 213 } else { 214 content = "<div class='doc'>" + content + "</div>"; 215 } 216 result.put(key, content); 217 } 218 return result; 219 } 220 221 @GET 222 @Produces("text/plain") 223 @Path("feedServices") 224 public String feedServices() throws JSONException { 225 List<String> serviceIds = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()).getServiceIds(); 226 227 Map<String, String> descs = getRenderedDescriptions("NXService"); 228 229 List<ArtifactLabel> serviceLabels = new ArrayList<ArtifactLabel>(); 230 231 for (String id : serviceIds) { 232 serviceLabels.add(ArtifactLabel.createLabelFromService(id)); 233 } 234 Collections.sort(serviceLabels); 235 236 JSONArray array = new JSONArray(); 237 238 for (ArtifactLabel label : serviceLabels) { 239 JSONObject object = new JSONObject(); 240 object.put("id", label.getId()); 241 object.put("label", label.getLabel()); 242 object.put("desc", descs.get(label.id)); 243 object.put("url", "http://explorer.nuxeo.org/nuxeo/site/distribution/current/service2Bundle/" + label.id); 244 array.put(object); 245 } 246 247 return array.toString(); 248 } 249 250 @GET 251 @Produces("text/plain") 252 @Path("feedExtensionPoints") 253 public String feedExtensionPoints() throws JSONException { 254 List<String> epIds = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()).getExtensionPointIds(); 255 256 Map<String, String> descs = getRenderedDescriptions("NXExtensionPoint"); 257 258 List<ArtifactLabel> labels = new ArrayList<ArtifactLabel>(); 259 260 for (String id : epIds) { 261 labels.add(ArtifactLabel.createLabelFromExtensionPoint(id)); 262 } 263 Collections.sort(labels); 264 265 JSONArray array = new JSONArray(); 266 267 for (ArtifactLabel label : labels) { 268 JSONObject object = new JSONObject(); 269 object.put("id", label.getId()); 270 object.put("label", label.getLabel()); 271 object.put("desc", descs.get(label.id)); 272 object.put("url", "http://explorer.nuxeo.org/nuxeo/site/distribution/current/extensionPoint2Component/" 273 + label.id); 274 array.put(object); 275 } 276 277 return array.toString(); 278 } 279 280 @GET 281 @Produces("text/html") 282 @Path("listContributions") 283 public Object getContributions() { 284 DistributionSnapshot snapshot = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()); 285 List<String> cIds = snapshot.getContributionIds(); 286 return getView("listContributions").arg("contributions", snapshot.getContributions()).arg( 287 Distribution.DIST_ID, ctx.getProperty(Distribution.DIST_ID)); 288 } 289 290 @GET 291 @Produces("text/html") 292 @Path("listExtensionPoints") 293 public Object getExtensionPoints() { 294 List<String> epIds = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()).getExtensionPointIds(); 295 296 List<ArtifactLabel> labels = epIds.stream().map(ArtifactLabel::createLabelFromExtensionPoint).collect(Collectors.toList()); 297 298 Collections.sort(labels); 299 return getView("listExtensionPoints").arg("eps", labels).arg(Distribution.DIST_ID, 300 ctx.getProperty(Distribution.DIST_ID)); 301 } 302 303 /** 304 * XXX Not used? 305 */ 306 @POST 307 @Produces("text/html") 308 @Path("filterComponents") 309 public Object filterComponents(@FormParam("fulltext") String fulltext) { 310 List<NuxeoArtifact> artifacts = getSearcher().filterArtifact(getContext().getCoreSession(), distributionId, 311 ComponentInfo.TYPE_NAME, fulltext); 312 313 List<ArtifactLabel> xmlLabels = new ArrayList<>(); 314 List<ArtifactLabel> javaLabels = new ArrayList<>(); 315 316 for (NuxeoArtifact item : artifacts) { 317 ComponentInfo ci = (ComponentInfo) item; 318 if (ci.isXmlPureComponent()) { 319 xmlLabels.add(ArtifactLabel.createLabelFromComponent(ci.getId())); 320 } else { 321 javaLabels.add(ArtifactLabel.createLabelFromComponent(ci.getId())); 322 } 323 } 324 return getView("listComponents").arg("javaComponents", javaLabels).arg("xmlComponents", xmlLabels).arg( 325 Distribution.DIST_ID, ctx.getProperty(Distribution.DIST_ID)).arg("searchFilter", fulltext); 326 } 327 328 /** 329 * XXX Not used? 330 */ 331 @POST 332 @Produces("text/html") 333 @Path("filterBundles") 334 public Object filterBundles(@FormParam("fulltext") String fulltext) { 335 List<NuxeoArtifact> artifacts = getSearcher().filterArtifact(getContext().getCoreSession(), distributionId, 336 BundleInfo.TYPE_NAME, fulltext); 337 List<String> bundleIds = new ArrayList<String>(); 338 for (NuxeoArtifact item : artifacts) { 339 bundleIds.add(item.getId()); 340 } 341 return getView("listBundles").arg("bundleIds", bundleIds).arg(Distribution.DIST_ID, 342 ctx.getProperty(Distribution.DIST_ID)).arg("searchFilter", fulltext); 343 } 344 345 /** 346 * XXX Not used? 347 */ 348 @POST 349 @Produces("text/html") 350 @Path("filterServices") 351 public Object filterServices() { 352 String fulltext = getContext().getForm().getFormProperty("fulltext"); 353 List<NuxeoArtifact> artifacts = getSearcher().filterArtifact(getContext().getCoreSession(), distributionId, 354 ServiceInfo.TYPE_NAME, fulltext); 355 List<String> serviceIds = new ArrayList<String>(); 356 for (NuxeoArtifact item : artifacts) { 357 serviceIds.add(item.getId()); 358 } 359 List<ArtifactLabel> serviceLabels = new ArrayList<ArtifactLabel>(); 360 361 for (String id : serviceIds) { 362 serviceLabels.add(ArtifactLabel.createLabelFromService(id)); 363 } 364 return getView("listServices").arg("services", serviceLabels).arg(Distribution.DIST_ID, 365 ctx.getProperty(Distribution.DIST_ID)).arg("searchFilter", fulltext); 366 } 367 368 @POST 369 @Produces("text/html") 370 @Path("filterExtensionPoints") 371 public Object filterExtensionPoints(@FormParam("fulltext") String fulltext) { 372 List<NuxeoArtifact> artifacts = getSearcher().filterArtifact(getContext().getCoreSession(), distributionId, 373 ExtensionPointInfo.TYPE_NAME, fulltext); 374 List<String> eps = artifacts.stream().map(NuxeoArtifact::getId).collect(Collectors.toList()); 375 List<ArtifactLabel> labels = eps.stream().map(ArtifactLabel::createLabelFromExtensionPoint).collect(Collectors.toList()); 376 return getView("listExtensionPoints").arg("eps", labels).arg(Distribution.DIST_ID, 377 ctx.getProperty(Distribution.DIST_ID)).arg("searchFilter", fulltext); 378 } 379 380 @POST 381 @Produces("text/html") 382 @Path("filterContributions") 383 public Object filterContributions(@FormParam("fulltext") String fulltext) { 384 List<NuxeoArtifact> artifacts = getSearcher().filterArtifact(getContext().getCoreSession(), distributionId, 385 ExtensionInfo.TYPE_NAME, fulltext); 386 return getView("listContributions").arg("contributions", artifacts).arg(Distribution.DIST_ID, 387 ctx.getProperty(Distribution.DIST_ID)).arg("searchFilter", fulltext); 388 } 389 390 @Path("doc") 391 public Resource viewDoc() { 392 return ctx.newObject("documentation"); 393 } 394 395 @GET 396 @Produces("text/html") 397 @Path("service2Bundle/{serviceId}") 398 public Object service2Bundle(@PathParam("serviceId") String serviceId) { 399 400 ServiceInfo si = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()).getService(serviceId); 401 if (si == null) { 402 return null; 403 } 404 String cid = si.getComponentId(); 405 406 ComponentInfo ci = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()).getComponent(cid); 407 String bid = ci.getBundle().getId(); 408 409 org.nuxeo.common.utils.Path target = new org.nuxeo.common.utils.Path(getContext().getRoot().getName()); 410 target = target.append(distributionId); 411 target = target.append("viewBundle"); 412 target = target.append(bid + "#Service." + serviceId); 413 try { 414 return Response.seeOther(new URI(target.toString())).build(); 415 } catch (URISyntaxException e) { 416 throw new NuxeoException(e); 417 } 418 } 419 420 @GET 421 @Produces("text/html") 422 @Path("extensionPoint2Component/{epId}") 423 public Object extensionPoint2Component(@PathParam("epId") String epId) { 424 425 ExtensionPointInfo epi = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()).getExtensionPoint( 426 epId); 427 if (epi == null) { 428 return null; 429 } 430 String cid = epi.getComponent().getId(); 431 432 org.nuxeo.common.utils.Path target = new org.nuxeo.common.utils.Path(getContext().getRoot().getName()); 433 target = target.append(distributionId); 434 target = target.append("viewComponent"); 435 target = target.append(cid + "#extensionPoint." + epId); 436 try { 437 return Response.seeOther(new URI(target.toString())).build(); 438 } catch (URISyntaxException e) { 439 throw new NuxeoException(e); 440 } 441 } 442 443 @Path("viewBundle/{bundleId}") 444 public Resource viewBundle(@PathParam("bundleId") String bundleId) { 445 NuxeoArtifactWebObject wo = (NuxeoArtifactWebObject) ctx.newObject("bundle", bundleId); 446 NuxeoArtifact nxItem = wo.getNxArtifact(); 447 if (nxItem == null) { 448 throw new WebResourceNotFoundException(bundleId); 449 } 450 TreeHelper.updateTree(getContext(), nxItem.getHierarchyPath()); 451 return wo; 452 } 453 454 @Path("viewComponent/{componentId}") 455 public Resource viewComponent(@PathParam("componentId") String componentId) { 456 NuxeoArtifactWebObject wo = (NuxeoArtifactWebObject) ctx.newObject("component", componentId); 457 NuxeoArtifact nxItem = wo.getNxArtifact(); 458 if (nxItem == null) { 459 throw new WebResourceNotFoundException(componentId); 460 } 461 TreeHelper.updateTree(getContext(), nxItem.getHierarchyPath()); 462 return wo; 463 } 464 465 @Path("viewSeamComponent/{componentId}") 466 public Resource viewSeamComponent(@PathParam("componentId") String componentId) { 467 return (NuxeoArtifactWebObject) ctx.newObject("seamComponent", componentId); 468 } 469 470 @Path("viewOperation/{opId}") 471 public Resource viewOperation(@PathParam("opId") String opId) { 472 return (NuxeoArtifactWebObject) ctx.newObject("operation", opId); 473 } 474 475 @Path("viewService/{serviceId}") 476 public Resource viewService(@PathParam("serviceId") String serviceId) { 477 NuxeoArtifactWebObject wo = (NuxeoArtifactWebObject) ctx.newObject("service", serviceId); 478 NuxeoArtifact nxItem = wo.getNxArtifact(); 479 if (nxItem == null) { 480 throw new WebResourceNotFoundException(serviceId); 481 } 482 TreeHelper.updateTree(getContext(), nxItem.getHierarchyPath()); 483 return wo; 484 } 485 486 @Path("viewExtensionPoint/{epId}") 487 public Resource viewExtensionPoint(@PathParam("epId") String epId) { 488 NuxeoArtifactWebObject wo = (NuxeoArtifactWebObject) ctx.newObject("extensionPoint", epId); 489 NuxeoArtifact nxItem = wo.getNxArtifact(); 490 if (nxItem == null) { 491 throw new WebResourceNotFoundException(epId); 492 } 493 TreeHelper.updateTree(getContext(), nxItem.getHierarchyPath()); 494 return wo; 495 } 496 497 @Path("viewContribution/{cId}") 498 public Resource viewContribution(@PathParam("cId") String cId) { 499 NuxeoArtifactWebObject wo = (NuxeoArtifactWebObject) ctx.newObject("contribution", cId); 500 NuxeoArtifact nxItem = wo.getNxArtifact(); 501 if (nxItem == null) { 502 throw new WebResourceNotFoundException(cId); 503 } 504 TreeHelper.updateTree(getContext(), nxItem.getHierarchyPath()); 505 return wo; 506 } 507 508 @Path("viewBundleGroup/{gId}") 509 public Resource viewBundleGroup(@PathParam("gId") String gId) { 510 NuxeoArtifactWebObject wo = (NuxeoArtifactWebObject) ctx.newObject("bundleGroup", gId); 511 NuxeoArtifact nxItem = wo.getNxArtifact(); 512 if (nxItem == null) { 513 throw new WebResourceNotFoundException(gId); 514 } 515 TreeHelper.updateTree(getContext(), nxItem.getHierarchyPath()); 516 return wo; 517 } 518 519 @Path("viewArtifact/{id}") 520 public Object viewArtifact(@PathParam("id") String id) { 521 DistributionSnapshot snap = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()); 522 523 BundleGroup bg = snap.getBundleGroup(id); 524 if (bg != null) { 525 return viewBundleGroup(id); 526 } 527 528 BundleInfo bi = snap.getBundle(id); 529 if (bi != null) { 530 return viewBundle(id); 531 } 532 533 ComponentInfo ci = snap.getComponent(id); 534 if (ci != null) { 535 return viewComponent(id); 536 } 537 538 ServiceInfo si = snap.getService(id); 539 if (si != null) { 540 return viewService(id); 541 } 542 543 ExtensionPointInfo epi = snap.getExtensionPoint(id); 544 if (epi != null) { 545 return viewExtensionPoint(id); 546 } 547 548 ExtensionInfo ei = snap.getContribution(id); 549 if (ei != null) { 550 return viewContribution(id); 551 } 552 553 return Response.status(404).build(); 554 } 555 556 public String getLabel(String id) { 557 return null; 558 } 559 560 @GET 561 @Produces("text/html") 562 @Path("listSeamComponents") 563 public Object listSeamComponents() { 564 return dolistSeamComponents("listSeamComponents", false); 565 } 566 567 @GET 568 @Produces("text/html") 569 @Path("listSeamComponentsSimple") 570 public Object listSeamComponentsSimple() { 571 return dolistSeamComponents("listSeamComponentsSimple", true); 572 } 573 574 protected Object dolistSeamComponents(String view, boolean hideNav) { 575 576 getSnapshotManager().initSeamContext(getContext().getRequest()); 577 578 DistributionSnapshot snap = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()); 579 List<SeamComponentInfo> seamComponents = snap.getSeamComponents(); 580 return getView(view).arg("seamComponents", seamComponents).arg(Distribution.DIST_ID, 581 ctx.getProperty(Distribution.DIST_ID)).arg("hideNav", Boolean.valueOf(hideNav)); 582 } 583 584 @GET 585 @Produces("text/html") 586 @Path("listOperations") 587 public Object listOperations() { 588 DistributionSnapshot snap = getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()); 589 List<OperationInfo> operations = snap.getOperations(); 590 return getView("listOperations").arg("operations", operations).arg(Distribution.DIST_ID, 591 ctx.getProperty(Distribution.DIST_ID)).arg("hideNav", Boolean.valueOf(false)); 592 } 593 594}