001/* Interface for parameters that provide web export content.
002
003 Copyright (c) 2011-2014 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028
029package ptolemy.vergil.basic.export.web;
030
031import java.awt.Frame;
032import java.awt.print.PrinterException;
033import java.io.File;
034import java.io.FileOutputStream;
035import java.io.IOException;
036import java.io.OutputStream;
037import java.util.HashMap;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042
043import ptolemy.actor.TypedActor;
044import ptolemy.actor.gui.Configuration;
045import ptolemy.actor.gui.Effigy;
046import ptolemy.actor.gui.PtolemyEffigy;
047import ptolemy.actor.gui.Tableau;
048import ptolemy.domains.modal.kernel.FSMActor;
049import ptolemy.domains.modal.kernel.State;
050import ptolemy.domains.modal.modal.ModalModel;
051import ptolemy.gui.ImageExportable;
052import ptolemy.kernel.util.IllegalActionException;
053import ptolemy.kernel.util.Instantiable;
054import ptolemy.kernel.util.NameDuplicationException;
055import ptolemy.kernel.util.NamedObj;
056import ptolemy.util.StringUtilities;
057import ptolemy.vergil.basic.ExportParameters;
058import ptolemy.vergil.basic.HTMLExportable;
059
060///////////////////////////////////////////////////////////////////
061//// LinkToOpenTableaux
062/**
063 * A parameter specifying default hyperlink to associate
064 * with icons in model. Putting this into a model causes a hyperlink
065 * to be associated with each icon (as specified by the <i>include</i>
066 * and <i>instancesOf</i> parameters) that is associated to an
067 * open Tableau.
068 * If the the frame associated with the tableau implements
069 * HTMLExportable, then this is an ordinary link to
070 * the HTML exported by the frame. If it instead
071 * implements ImageExportable, then this a link that
072 * brings up the image in a lightbox.
073 * <p>
074 * This parameter is designed to be included in a Configuration file
075 * to specify global default behavior for export to Web. Just put
076 * it in the top level of the Configuration, and this hyperlink
077 * will be provided by default.
078 * <p>
079 * Note that this class works closely with
080 * {@link ptolemy.vergil.basic.export.html.ExportHTMLAction}.
081 * It will not work if the {@link WebExporter} provided to its
082 * methods is not an instance of ExportHTMLAction.
083 *
084 * @author Edward A. Lee
085 * @version $Id$
086 * @since Ptolemy II 10.0
087 * @Pt.ProposedRating Red (cxh)
088 * @Pt.AcceptedRating Red (cxh)
089 */
090public class LinkToOpenTableaux extends DefaultIconLink {
091
092    /** Create an instance of this parameter.
093     *  @param container The container.
094     *  @param name The name.
095     *  @exception IllegalActionException If the superclass throws it.
096     *  @exception NameDuplicationException If the superclass throws it.
097     */
098    public LinkToOpenTableaux(NamedObj container, String name)
099            throws IllegalActionException, NameDuplicationException {
100        super(container, name);
101
102        _exportedClassDefinitions = null;
103    }
104
105    ///////////////////////////////////////////////////////////////////
106    ////                         public methods                    ////
107
108    /** Provide content to the specified web exporter to be
109     *  included in a web page for the container of this object.
110     *  This overrides the base class to ensure that each class
111     *  definition is exported only once.
112     *  @exception IllegalActionException If a subclass throws it.
113     */
114    @Override
115    public void provideContent(WebExporter exporter)
116            throws IllegalActionException {
117        try {
118            _exportedClassDefinitions = new HashSet<NamedObj>();
119            super.provideContent(exporter);
120        } finally {
121            _exportedClassDefinitions = null;
122        }
123    }
124
125    ///////////////////////////////////////////////////////////////////
126    ////                         protected methods                 ////
127
128    /** Override the base class to generate a web page or an image
129     *  file for the specified object, if appropriate, and to provide
130     *  the href, target, and class attributes to the area attribute
131     *  associated with the object.
132     *  @param exporter The exporter.
133     *  @param object The Ptolemy II object.
134     *  @exception IllegalActionException If evaluating parameters fails.
135     */
136    @Override
137    protected void _provideEachAttribute(WebExporter exporter, NamedObj object)
138            throws IllegalActionException {
139        WebAttribute webAttribute;
140
141        // Create a table of effigies associated with any
142        // open submodel or plot.
143        Map<NamedObj, PtolemyEffigy> openEffigies = new HashMap<NamedObj, PtolemyEffigy>();
144        Tableau myTableau = exporter.getFrame().getTableau();
145        Effigy myEffigy = (Effigy) myTableau.getContainer();
146        List<PtolemyEffigy> effigies = myEffigy.entityList(PtolemyEffigy.class);
147        for (PtolemyEffigy effigy : effigies) {
148            // The following will, for example, associate a plotter with effigy
149            // for the open plot.
150            openEffigies.put(effigy.getModel(), effigy);
151        }
152        // In order to ensure that all open windows have hyperlinks
153        // to them, fix up the openEffies data structure so that if
154        // the container of a plotter (for example) is not associated
155        // with an open effigy, then we attempt to associate it with
156        // the container instead, or the container's container, until
157        // we get to the top level.
158        Map<NamedObj, PtolemyEffigy> containerAssociations = new HashMap<NamedObj, PtolemyEffigy>();
159        if (openEffigies.size() > 0) {
160            for (NamedObj component : openEffigies.keySet()) {
161                if (component == null) {
162                    // This is caused by $PTII/bin/ptinvoke
163                    // ptolemy.vergil.basic.export.ExportModel -force
164                    // htm -run -openComposites -whiteBackground
165                    // ptolemy/actor/gt/demo/ModelExecution/ModelExecution.xml
166                    // $PTII/ptolemy/actor/gt/demo/ModelExecution/ModelExecution
167                    System.out.println(
168                            "Warning: LinkToOpenTableaux._provideEachAttribute() "
169                                    + object.getFullName()
170                                    + ", an open effigy was null?");
171                } else {
172                    NamedObj container = component.getContainer();
173                    while (container != null) {
174                        if (openEffigies.get(container) != null) {
175                            // The container of a plotter (say) has
176                            // an open effigy, so there will be a link
177                            // to the plot.
178                            // Exporting
179                            // ptolemy/data/ontologies/demo/CarTracking/CarTracking.xml
180                            // was hanging in a tight loop here.
181                            container = container.getContainer();
182                            continue;
183                        }
184                        // The container of the plotter (say) does
185                        // not have an open effigy.  Associate this
186                        // container and then try the container's container.
187                        containerAssociations.put(container,
188                                openEffigies.get(component));
189                        container = container.getContainer();
190                    }
191                }
192            }
193        }
194        openEffigies.putAll(containerAssociations);
195
196        PtolemyEffigy effigy = openEffigies.get(object);
197        // The hierarchy of effigies does not always follow the model hierarchy
198        // (e.g., a PlotEffigy will be contained by the top-level effigy for the
199        // model for some reason), so if the effigy is null, we search
200        // nonetheless for an effigy.
201        if (effigy == null) {
202            Effigy candidate = Configuration.findEffigy(object);
203            if (candidate instanceof PtolemyEffigy) {
204                effigy = (PtolemyEffigy) candidate;
205            }
206        }
207        try {
208            if (effigy != null) {
209                // _linkTo() recursively calls writeHTML();
210                _linkTo(exporter, effigy, object, object,
211                        exporter.getExportParameters());
212            } else {
213                // If the object is a State, we still have work to do.
214                if (object instanceof State) {
215                    // In a ModalModel, object is a State
216                    // inside the _Controller.  But the effigy is stored
217                    // under the refinements of that state, which have the
218                    // same container as the _Controller.
219                    try {
220                        TypedActor[] refinements = ((State) object)
221                                .getRefinement();
222                        // FIXME: There may be more
223                        // than one refinement. How to open all of them?
224                        // We have only one link. For now, just open the first one.
225                        if (refinements != null && refinements.length > 0) {
226                            effigy = openEffigies.get(refinements[0]);
227                            if (effigy != null) {
228                                // _linkTo() recursively calls writeHTML();
229                                _linkTo(exporter, effigy, object,
230                                        (NamedObj) refinements[0],
231                                        exporter.getExportParameters());
232                            }
233                        }
234                    } catch (IllegalActionException e) {
235                        // Ignore errors here. Just don't export this refinement.
236                    }
237                } else if (object instanceof Instantiable) {
238                    // There is no open effigy, but the object might
239                    // be an instance of a class where the class definition
240                    // is open. Look for that.
241                    Instantiable parent = ((Instantiable) object).getParent();
242                    if (parent instanceof NamedObj) {
243                        // Avoid doing the export of a class definition
244                        // multiple times.
245                        if (_exportedClassDefinitions.contains(parent)) {
246                            // Have already exported the class definition. Just
247                            // need to add the hyperlink.
248                            webAttribute = WebAttribute.createWebAttribute(
249                                    object, "hrefWebAttribute", "href");
250                            webAttribute.setExpression(
251                                    parent.getName() + "/index.html");
252                            exporter.defineAttribute(webAttribute, true);
253                        } else {
254                            // Have not exported the class definition. Do so now.
255                            _exportedClassDefinitions.add((NamedObj) parent);
256                            Effigy classEffigy = Configuration
257                                    .findEffigy((NamedObj) parent);
258                            if (classEffigy instanceof PtolemyEffigy) {
259                                // _linkTo() recursively calls writeHTML();
260                                _linkTo(exporter, (PtolemyEffigy) classEffigy,
261                                        object, (NamedObj) parent,
262                                        exporter.getExportParameters());
263                            }
264                        }
265                    }
266                }
267            }
268        } catch (Throwable throwable) {
269            throw new IllegalActionException(this, throwable,
270                    "Failed to generate sub-web-page. ");
271        }
272    }
273
274    ///////////////////////////////////////////////////////////////////
275    ////                         private methods                   ////
276
277    /** Return the title of the specified object. If it contains a parameter
278     *  of class {@link Title}, then return the title specified by that class.
279     *  Otherwise, if the object is an instance of FSMActor contained by
280     *  a ModalModel, then return the
281     *  name of its container, not the name of the FSMActor.
282     *  Otherwise, return the name of the object.
283     *  @param object The object.
284     *  @return A title for the object.
285     *  @exception IllegalActionException If accessing the title attribute fails..
286     */
287    private static String _getTitleText(NamedObj object)
288            throws IllegalActionException {
289        // If the object contains an IconLink parameter, then use that instead
290        // of the default. If it has more than one, then just use the first one.
291        List<Title> links = object.attributeList(Title.class);
292        if (links != null && links.size() > 0) {
293            return links.get(0).stringValue();
294        }
295        if (object instanceof FSMActor) {
296            NamedObj container = object.getContainer();
297            if (container instanceof ModalModel) {
298                return container.getName();
299            }
300        }
301        return object.getName();
302    }
303
304    /** For the specified effigy, define the relevant href, target,
305     *  and class area attributes
306     *  if the effigy has any open tableaux and those have frames
307     *  that implement either HTMLExportable or ImageExportable.
308     *  As a side effect, this may generate HTML or image
309     *  files or subdirectories in the directory given in the specified
310     *  parameters.
311     *  @param exporter The exporter.
312     *  @param effigy The effigy.
313     *  @param sourceObject The source Ptolemy II object (link from).
314     *  @param destinationObject The destination object (link to, same as sourceObject,
315     *   or alternatively, a class definition for sourceObject).
316     *  @param parameters The parameters of the web export that requires this link.
317     *  @exception IOException If unable to create required HTML files.
318     *  @exception PrinterException If unable to create required HTML files.
319     *  @exception IllegalActionException If something goes wrong.
320     */
321    private void _linkTo(WebExporter exporter, PtolemyEffigy effigy,
322            NamedObj sourceObject, NamedObj destinationObject,
323            ExportParameters parameters)
324            throws IOException, PrinterException, IllegalActionException {
325        File gifFile;
326        WebAttribute webAttribute;
327        WebElement webElement;
328        // Look for any open tableaux for the object.
329        List<Tableau> tableaux = effigy.entityList(Tableau.class);
330
331        // ThreadedComposite extends MirrorComposite.
332        // ThreadedComposites do not have a top level tableau, they
333        // contain an effigy that contains a tableau.
334
335        // To replicate:
336        // $PTII/bin/ptinvoke ptolemy.vergil.basic.export.ExportModel -force htm -run -openComposites -timeOut 30000 -whiteBackground ptolemy/actor/lib/hoc/demo/ThreadedComposite/MulticoreExecution.xml $PTII/ptolemy/actor/lib/hoc/demo/ThreadedComposite/MulticoreExecution
337
338        if (tableaux.size() == 0) {
339            List<PtolemyEffigy> effigies = effigy
340                    .entityList(PtolemyEffigy.class);
341            if (effigies != null && effigies.size() > 0) {
342                tableaux = effigies.get(0).entityList(Tableau.class);
343            }
344        }
345        // If there are multiple tableaux open, use only the first one.
346        if (tableaux.size() > 0) {
347            // The ddf IfThenElse model has a composite called +1/-1 Gain,
348            // which is not a legal file name, so we sanitize it.
349            String name = StringUtilities
350                    .sanitizeName(destinationObject.getName());
351            Frame frame = tableaux.get(0).getFrame();
352            // If it's a composite actor, export HTML.
353            if (frame instanceof HTMLExportable) {
354                File directory = parameters.directoryToExportTo;
355                File subDirectory = new File(directory, name);
356                if (subDirectory.exists()) {
357                    if (!subDirectory.isDirectory()) {
358                        // Move file out of the way.
359                        File backupFile = new File(directory, name + ".bak");
360                        if (!subDirectory.renameTo(backupFile)) {
361                            throw new IOException(
362                                    "Failed to rename \"" + subDirectory
363                                            + "\" to \"" + backupFile + "\"");
364                        }
365                    }
366                } else if (!subDirectory.mkdir()) {
367                    throw new IOException(
368                            "Unable to create directory " + subDirectory);
369                }
370                ExportParameters newParameters = new ExportParameters(
371                        subDirectory, parameters);
372                // The null argument causes the write to occur to an index.html
373                // file.
374                ((HTMLExportable) frame).writeHTML(newParameters, null);
375                webAttribute = WebAttribute.createWebAttribute(sourceObject,
376                        "hrefWebAttribute", "href");
377                webAttribute.setExpression(name + "/index.html");
378                exporter.defineAttribute(webAttribute, true);
379
380                // Add to table of contents file if we are using the Ptolemy
381                // website infrastructure.
382                boolean usePtWebsite = Boolean.valueOf(StringUtilities
383                        .getProperty("ptolemy.ptII.exportHTML.usePtWebsite"));
384                if (usePtWebsite) {
385                    String destinationTitle = LinkToOpenTableaux
386                            ._getTitleText(destinationObject);
387                    if (destinationTitle.length() > 16) {
388                        //Truncate the text so that it does not overflow the toc.
389                        destinationTitle = destinationTitle.substring(0, 16)
390                                + ".";
391                    }
392                    webElement = WebElement.createWebElement(destinationObject,
393                            "tocContents", "tocContents");
394                    webElement.setExpression(
395                            " <li><a href=\"" + name + "/index.html" + "\">"
396                                    + destinationTitle + "</a></li>");
397                    exporter.defineElement(webElement, false);
398                }
399            } else if (frame instanceof ImageExportable) {
400                gifFile = new File(parameters.directoryToExportTo,
401                        name + ".gif");
402                if (parameters.deleteFilesOnExit) {
403                    gifFile.deleteOnExit();
404                }
405                OutputStream gifOut = new FileOutputStream(gifFile);
406                try {
407                    ((ImageExportable) frame).writeImage(gifOut, "gif");
408                } finally {
409                    gifOut.close();
410                }
411                // Strangely, the class has to be "iframe".
412                // I don't understand why it can't be "lightbox".
413                webAttribute = WebAttribute.createWebAttribute(sourceObject,
414                        "hrefWebAttribute", "href");
415                webAttribute.setExpression(name + ".gif");
416                exporter.defineAttribute(webAttribute, true);
417
418                webAttribute = WebAttribute.appendToWebAttribute(sourceObject,
419                        "classWebAttribute", "class", "iframe");
420
421                exporter.defineAttribute(webAttribute, true);
422            }
423        }
424    }
425
426    ///////////////////////////////////////////////////////////////////
427    ////                         private methods                   ////
428
429    /** A set of class definitions for which an export has already occurred. */
430    private Set<NamedObj> _exportedClassDefinitions;
431}