001/* 002 * Copyright (c) 2015 The Regents of the University of California. 003 * All rights reserved. 004 * 005 * '$Author: crawl $' 006 * '$Date: 2017-05-11 11:19:04 -0700 (Thu, 11 May 2017) $' 007 * '$Revision: 1183 $' 008 * 009 * Permission is hereby granted, without written agreement and without 010 * license or royalty fees, to use, copy, modify, and distribute this 011 * software and its documentation for any purpose, provided that the above 012 * copyright notice and the following two paragraphs appear in all copies 013 * of this software. 014 * 015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 019 * SUCH DAMAGE. 020 * 021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 026 * ENHANCEMENTS, OR MODIFICATIONS. 027 * 028 */ 029package org.kepler.webview.actor; 030 031import java.io.File; 032import java.io.IOException; 033import java.util.HashSet; 034import java.util.List; 035import java.util.Map; 036import java.util.Set; 037 038import org.apache.commons.io.FileUtils; 039import org.kepler.provenance.ProvenanceRecorder; 040import org.kepler.webview.data.TokenConverter; 041import org.kepler.webview.server.WebViewId; 042import org.kepler.webview.server.WebViewServer; 043import org.kepler.webview.server.WebViewableUtilities; 044 045import io.vertx.core.eventbus.EventBus; 046import io.vertx.core.eventbus.MessageConsumer; 047import io.vertx.core.json.JsonObject; 048import ptolemy.actor.ActorFiringListener; 049import ptolemy.actor.CompositeActor; 050import ptolemy.actor.FiringEvent; 051import ptolemy.actor.FiringsRecordable; 052import ptolemy.actor.IOPort; 053import ptolemy.actor.IOPortEvent; 054import ptolemy.actor.IOPortEventListener; 055import ptolemy.actor.Initializable; 056import ptolemy.actor.gui.style.TextStyle; 057import ptolemy.data.Token; 058import ptolemy.data.expr.FileParameter; 059import ptolemy.data.expr.Parameter; 060import ptolemy.data.expr.StringParameter; 061import ptolemy.kernel.Entity; 062import ptolemy.kernel.util.Attribute; 063import ptolemy.kernel.util.ChangeRequest; 064import ptolemy.kernel.util.IllegalActionException; 065import ptolemy.kernel.util.NameDuplicationException; 066import ptolemy.kernel.util.NamedObj; 067import ptolemy.kernel.util.Settable; 068import ptolemy.kernel.util.StringAttribute; 069import ptolemy.kernel.util.Workspace; 070import ptolemy.util.MessageHandler; 071 072public class WebViewAttribute extends StringAttribute implements Initializable, WebViewable, IOPortEventListener, ActorFiringListener { 073 074 public WebViewAttribute(NamedObj container, String name) throws IllegalActionException, NameDuplicationException { 075 super(container, name); 076 077 setExpression("Click configure."); 078 setVisibility(Settable.NOT_EDITABLE); 079 080 title = new StringParameter(this, "title"); 081 title.setExpression(""); 082 083 htmlFile = new FileParameter(this, "htmlFile"); 084 // TODO set base directory? 085 086 html = new StringParameter(this, "html"); 087 html.setToken("Click configure to edit."); 088 html.setVisibility(Settable.NOT_EDITABLE); 089 090 htmlCode = new StringAttribute(html, "htmlCode"); 091 TextStyle style = new TextStyle(htmlCode, "_style"); 092 style.height.setToken("100"); 093 094 } 095 096 @Override 097 public void addInitializable(Initializable initializable) { 098 _initializables.add(initializable); 099 } 100 101 /** React to an attribute change. */ 102 @Override 103 public void attributeChanged(Attribute attribute) throws IllegalActionException { 104 105 if (attribute == title) { 106 _setTitle(title.stringValue()); 107 } else { 108 super.attributeChanged(attribute); 109 } 110 } 111 112 /** Clone the actor into the specified workspace. 113 * @param workspace The workspace for the new object. 114 * @return A new actor. 115 * @exception CloneNotSupportedException If a derived class contains 116 * an attribute that cannot be cloned. 117 */ 118 @Override 119 public Object clone(Workspace workspace) throws CloneNotSupportedException { 120 WebViewAttribute newObject = (WebViewAttribute) super.clone(workspace); 121 newObject._consumer = null; 122 newObject._initializables = new HashSet<Initializable>(); 123 newObject._readDataJson = new JsonObject(); 124 //newObject._registeredName = null; 125 newObject._tokenConverter = new TokenConverter(); 126 return newObject; 127 } 128 129 @Override 130 public void firingEvent(FiringEvent event) { 131 132 if(event.getType() == FiringEvent.AFTER_ITERATE || 133 event.getType() == FiringEvent.AFTER_POSTFIRE) { 134 try { 135 //System.out.println("After fire event: " + event); 136 WebViewableUtilities.sendData(_readDataJson, this); 137 } catch (IllegalActionException e) { 138 System.err.println("Error sending data: " + e.getMessage()); 139 e.printStackTrace(System.err); 140 } 141 _readDataJson = new JsonObject(); 142 } 143 } 144 145 @Override 146 public String getHTML() throws IOException, IllegalActionException { 147 String htmlStr = null; 148 String fileStr = htmlFile.stringValue(); 149 150 // see if file was specified 151 if(fileStr == null) { 152 // return string in htmlCode 153 htmlStr = htmlCode.getExpression(); 154 } else if(fileStr.trim().isEmpty()) { 155 throw new IllegalActionException(this, 156 "Either htmlFile or htmlCode must be specified."); 157 } else { 158 File file = new File(fileStr); 159 // see if file is an absolute path 160 if(file.isAbsolute()) { 161 if(file.exists()) { 162 htmlStr = FileUtils.readFileToString(file); 163 } 164 } else { 165 // read relative path from <module>/resources/html 166 String path = WebViewServer.findFile(fileStr); 167 if(path != null) { 168 htmlStr = FileUtils.readFileToString(new File(path)); 169 } else { 170 throw new IllegalActionException(this, "Could not find file " + fileStr); 171 } 172 } 173 } 174 return htmlStr; 175 } 176 177 @Override 178 public List<Parameter> getOptions() { 179 return getContainer().attributeList(Parameter.class); 180 } 181 182 @Override 183 public Parameter getOption(String name) { 184 return (Parameter) getContainer().getAttribute(name); 185 } 186 187 @Override 188 public void initialize() throws IllegalActionException { 189 for(Initializable initializable : _initializables) { 190 initializable.initialize(); 191 } 192 } 193 194 @Override 195 public void portEvent(IOPortEvent event) throws IllegalActionException { 196 // read input ports and send to clients 197 if(event.getEventType() == IOPortEvent.GET_END) { 198 Object value = _convertInputToJson(event.getToken()); 199 //System.out.println("Port event " + event.getPort().getName() + " value is " + value); 200 _readDataJson.put(event.getPort().getName(), value); 201 } 202 203 if(!_firingEventsFromDirector) { 204 try { 205 //System.out.println("After fire event: " + event); 206 WebViewableUtilities.sendData(_readDataJson, this); 207 } catch (IllegalActionException e) { 208 System.err.println("Error sending data: " + e.getMessage()); 209 e.printStackTrace(System.err); 210 } 211 _readDataJson = new JsonObject(); 212 } 213 } 214 215 @Override 216 public void preinitialize() throws IllegalActionException { 217 for(Initializable initializable : _initializables) { 218 initializable.preinitialize(); 219 } 220 221 NamedObj container = getContainer(); 222 if(container instanceof Entity) { 223 for(Object object : ((Entity<?>)container).portList()) { 224 if(object instanceof IOPort) { 225 ((IOPort)object).addIOPortEventListener(this); 226 } 227 } 228 } 229 230 while(container != null && !(container instanceof CompositeActor)) { 231 container = container.getContainer(); 232 } 233 234 if(container == null || 235 !ProvenanceRecorder.containsSupportedDirector((CompositeActor) container)) { 236 _firingEventsFromDirector = false; 237 } else { 238 _firingEventsFromDirector = true; 239 } 240 241 } 242 243 @Override 244 public void removeInitializable(Initializable initializable) { 245 _initializables.remove(initializable); 246 } 247 248 /** Set the container. */ 249 @Override 250 public void setContainer(NamedObj container) throws IllegalActionException, NameDuplicationException { 251 252 NamedObj oldContainer = getContainer(); 253 254 super.setContainer(container); 255 256 if(container == null) { 257 unregisterHandler(); 258 } else { 259 _registerHandler(); 260 } 261 262 if(oldContainer != null) { 263 if(oldContainer instanceof Initializable) { 264 ((Initializable)oldContainer).removeInitializable(this); 265 } 266 267 if(oldContainer instanceof FiringsRecordable) { 268 ((FiringsRecordable)oldContainer).removeActorFiringListener(this); 269 } 270 } 271 272 if(container != null) { 273 if(container instanceof Initializable) { 274 ((Initializable)container).addInitializable(this); 275 } 276 277 if(container instanceof FiringsRecordable) { 278 ((FiringsRecordable)container).addActorFiringListener(this); 279 } 280 } 281 282 } 283 284 /* FIXME _registerHandler no longer uses name 285 @Override 286 public void setName(String name) throws IllegalActionException, 287 NameDuplicationException { 288 super.setName(name); 289 _registerHandler(); 290 } 291 */ 292 293 /** Stop listening for events from clients. This must be called when 294 * the containing model is no longer used. 295 */ 296 public void unregisterHandler() { 297 if(_consumer != null) { 298 _consumer.unregister(); 299 _consumer = null; 300 //System.out.println("unregistering handler for " + getFullName()); 301 } 302 } 303 304 @Override 305 public void wrapup() throws IllegalActionException { 306 for(Initializable initializable : _initializables) { 307 initializable.wrapup(); 308 } 309 310 NamedObj container = getContainer(); 311 if(container instanceof Entity) { 312 for(Object object : ((Entity<?>)container).portList()) { 313 if(object instanceof IOPort) { 314 ((IOPort)object).removeIOPortEventListener(this); 315 } 316 } 317 } 318 319 _readDataJson = new JsonObject(); 320 } 321 322 public FileParameter htmlFile; 323 public StringParameter html; 324 public StringAttribute htmlCode; 325 public StringParameter title; 326 327 /////////////////////////////////////////////////////////////////// 328 //// protected methods //// 329 330 protected void _registerHandler() throws IllegalActionException { 331 332 // TODO check if address changed. maybe: address does not include actor name. 333 unregisterHandler(); 334 335 // do not register if there is no container 336 if(toplevel() == this) { 337 return; 338 } 339 340 EventBus bus = WebViewServer.vertx().eventBus(); 341 final String id = WebViewId.getId(this); 342 //final String actorPath = "actor-" + id; 343 344 /* 345 if(_registeredName != null && !_registeredName.equals(id)) { 346 // send rename to web clients 347 WebViewableUtilities.sendRenameActor(_registeredName, this); 348 } 349 350 _registeredName = id; 351 */ 352 353 String handlerAddress = "/ws/" + WebViewId.getId(toplevel()); 354 //System.out.println(getFullName() + ": " + id + " registering handler at " + _handlerAddress); 355 356 _consumer = bus.consumer(handlerAddress, message -> { 357 JsonObject json = message.body(); 358 //System.out.println(id + " recv from web: " + json); 359 JsonObject actor = json.getJsonObject("actor"); 360 if(actor != null) { 361 JsonObject pathObject = actor.getJsonObject(id); 362 if(pathObject != null) { 363 JsonObject data = pathObject.getJsonObject("data"); 364 if(data != null) { 365 for(Map.Entry<String, Object> entry: data) { 366 _dataReceived(entry.getKey(), entry.getValue()); 367 } 368 } 369 JsonObject options = pathObject.getJsonObject("options"); 370 if(options != null) { 371 for(Map.Entry<String,Object> entry: options) { 372 _optionReceived(entry.getKey(), entry.getValue()); 373 } 374 } 375 JsonObject newPos = pathObject.getJsonObject("pos"); 376 if(newPos != null) { 377 //System.out.println(getFullName() + " got pos: " + newPos); 378 StringAttribute position = (StringAttribute) getAttribute("_webWindowProperties"); 379 try { 380 if(position == null) { 381 position = new StringAttribute(WebViewAttribute.this, "_webWindowProperties"); 382 // set visibility to expert to hide in configure dialog 383 position.setVisibility(Settable.EXPERT); 384 } 385 String curStr = position.getValueAsString(); 386 if(!curStr.isEmpty()) { 387 JsonObject curPos = new JsonObject(position.getValueAsString()); 388 curPos.mergeIn(newPos); 389 position.setExpression(curPos.encode()); 390 } else { 391 position.setExpression(newPos.encode()); 392 } 393 394 } catch(IllegalActionException | NameDuplicationException e) { 395 MessageHandler.error("Error setting window position.", e); 396 } 397 } 398 } 399 } 400 401 JsonObject event = json.getJsonObject("event"); 402 if(event != null) { 403 if(!event.containsKey("type")) { 404 System.err.println("ERROR: missing type for event: " + event); 405 } else { 406 String type = event.getString("type"); 407 try { 408 switch(type) { 409 case "conopen": 410 _connectionOpened(event.getString("id")); 411 break; 412 case "conclose": 413 _connectionClosed(event.getString("id")); 414 break; 415 default: 416 System.err.println("WARNING: unknown event type : " + type); 417 break; 418 } 419 } catch(IllegalActionException e) { 420 MessageHandler.error("Error handling " + type + " event.", e); 421 } 422 } 423 } 424 }); 425 426 } 427 428 protected void _connectionClosed(String id) { 429 430 } 431 432 protected void _connectionOpened(String id) throws IllegalActionException { 433 434 // set parameters 435 try { 436 WebViewableUtilities.sendOptions(this, id); 437 } catch (IllegalActionException e) { 438 System.err.println("Error sending options: " + e.getMessage()); 439 } 440 441 // send initialize event 442 WebViewableUtilities.sendEvent(WebViewableUtilities.Event.Initialize, this, id); 443 } 444 445 protected void _dataReceived(String name, Object value) { 446 NamedObj container = getContainer(); 447 if(container instanceof WebView) { 448 ((WebView)container).dataReceived(name, value); 449 } 450 } 451 452 protected void _optionReceived(String name, final Object value) { 453 454 final Parameter option = getOption(name); 455 456 if(option == null) { 457 System.err.println("WARNING: no parameter found for option received: " + name); 458 } else { 459 // if not in batch mode, update canvas 460 String headless = System.getProperty("java.awt.headless"); 461 boolean usingUI = headless == null || headless.equals("false"); 462 463 ChangeRequest request = new ChangeRequest(this, 464 "WebViewAttribute change request", 465 usingUI) { 466 @Override 467 protected void _execute() throws IllegalActionException { 468 Token oldToken = option.getToken(); 469 470 String newValue = value.toString(); 471 if (oldToken == null || !oldToken.toString().equals(newValue)) { 472 option.setToken(newValue); 473 474 // NOTE: If we don't call validate(), then the 475 // change will not propagate to dependents. 476 option.validate(); 477 } 478 } 479 }; 480 request.setPersistent(false); 481 //request.addChangeListener(this); 482 requestChange(request); 483 } 484 } 485 486 protected Object _convertInputToJson(Token token) throws IllegalActionException { 487 return _tokenConverter.convertFromToken(token); 488 } 489 490 /////////////////////////////////////////////////////////////////// 491 //// private methods //// 492 493 /** Set the title of the actor in the web view. */ 494 private void _setTitle(String titleStr) throws IllegalActionException { 495 496 if(_titleStr == null || !_titleStr.equals(titleStr)) { 497 498 // if title is empty, use actor name. 499 if(titleStr.trim().isEmpty()) { 500 _titleStr = getName(); 501 } else { 502 _titleStr = titleStr; 503 } 504 WebViewableUtilities.sendTitle(_titleStr, this); 505 } 506 } 507 508 /////////////////////////////////////////////////////////////////// 509 //// private fields //// 510 511 private MessageConsumer<JsonObject> _consumer; 512 513 /** The title of the window in the web view. */ 514 private String _titleStr; 515 516 private Set<Initializable> _initializables = new HashSet<Initializable>(); 517 private JsonObject _readDataJson = new JsonObject(); 518 519 private TokenConverter _tokenConverter = new TokenConverter(); 520 521 /** If true, director will send firing events. */ 522 private boolean _firingEventsFromDirector = false; 523}