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}