001/* An action for getting documentation.
002
003 Copyright (c) 2006-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 */
028package ptolemy.vergil.basic;
029
030import java.awt.event.ActionEvent;
031import java.net.URL;
032import java.util.Iterator;
033import java.util.List;
034
035import javax.swing.JOptionPane;
036
037import ptolemy.actor.gui.Configuration;
038import ptolemy.actor.gui.Effigy;
039import ptolemy.actor.gui.Tableau;
040import ptolemy.data.expr.Parameter;
041import ptolemy.kernel.ComponentEntity;
042import ptolemy.kernel.attributes.VersionAttribute;
043import ptolemy.kernel.util.InternalErrorException;
044import ptolemy.kernel.util.KernelException;
045import ptolemy.kernel.util.KernelRuntimeException;
046import ptolemy.kernel.util.NamedObj;
047import ptolemy.kernel.util.StringAttribute;
048import ptolemy.util.MessageHandler;
049import ptolemy.vergil.actor.DocApplicationSpecializer;
050import ptolemy.vergil.actor.DocBuilderEffigy;
051import ptolemy.vergil.actor.DocBuilderTableau;
052import ptolemy.vergil.actor.DocEffigy;
053import ptolemy.vergil.actor.DocManager;
054import ptolemy.vergil.actor.DocTableau;
055import ptolemy.vergil.toolbox.FigureAction;
056
057///////////////////////////////////////////////////////////////////
058//// GetDocumentationAction
059
060/** This is an action that accesses the documentation for a Ptolemy
061    object associated with a figure.  Note that this base class does
062    not put this action in a menu, since some derived classes will
063    not want it.  But by having it here, it is available to all
064    derived classes.
065
066    This class provides an action for removing instance-specific documentation.
067
068    @author Edward A. Lee
069    @version $Id$
070    @since Ptolemy II 5.2
071    @Pt.ProposedRating Red (eal)
072    @Pt.AcceptedRating Red (johnr)
073 */
074@SuppressWarnings("serial")
075public class GetDocumentationAction extends FigureAction {
076
077    /** Construct an instance and give a preference for whether the
078     * KeplerDocumentationAttribute or the docAttribute should be displayed
079     * if both exist.
080     * @param docPreference 0 for docAttribute, 1 for
081     * KeplerDocumentationAttribute
082     */
083    public GetDocumentationAction(int docPreference) {
084        super("Get Documentation");
085        _docPreference = docPreference;
086    }
087
088    /** Construct an instance of this action. */
089    public GetDocumentationAction() {
090        super("Get Documentation");
091    }
092
093    ///////////////////////////////////////////////////////////////////
094    ////                         public methods                    ////
095
096    /** Perform the action by opening documentation for the target.
097     *  In the default situation, the documentation is in doc.codeDoc.
098     *  However, if we have a custom application like HyVisual,
099     *  VisualSense or Viptos, then we create the docs in
100     *  doc.codeDoc<i>ApplicationName</i>.doc.codeDoc.  However, this
101     *  directory gets jar up and shipped with these apps when we ship
102     *  windows installers and the docs are found at doc.codeDoc
103     *  again.  So, if _applicationName is set, we look in
104     *  doc.codeDoc<i>_applicationName</i>.doc.codeDoc.  If that is
105     *  not found, we look in doc.codeDoc.  If that is not found,
106     *  we bring up {@link ptolemy.vergil.actor.DocBuilderGUI}.
107     */
108    @Override
109    public void actionPerformed(ActionEvent e) {
110        super.actionPerformed(e);
111
112        if (_configuration == null) {
113            MessageHandler
114                    .error("Cannot get documentation without a configuration.");
115        }
116
117        NamedObj target = getTarget();
118        if (target == null) {
119            // Ignore and return.
120            return;
121        }
122
123        showDocumentation(target);
124    }
125
126    /**
127     * Show the documentation for a NamedObj.  This does the same
128     * thing as the actionPerformed but without the action handler
129     * @param target The NamedObj that will have its documentation shown.
130     */
131    public void showDocumentation(NamedObj target) {
132        if (_configuration == null) {
133            MessageHandler
134                    .error("Cannot get documentation without a configuration.");
135        }
136
137        // If the object contains
138        // an attribute named of class DocAttribute or if there
139        // is a doc file for the object in the standard place,
140        // then use the DocViewer class to display the documentation.
141        // For backward compatibility, if neither of these is found,
142        // then we open the Javadoc file, if it is found.
143        List docAttributes = target.attributeList(DocAttribute.class);
144        //check for the KeplerDocumentation attribute
145        KeplerDocumentationAttribute keplerDocumentationAttribute = (KeplerDocumentationAttribute) target
146                .getAttribute("KeplerDocumentation");
147        int docAttributeSize = docAttributes.size();
148
149        if (docAttributes.size() != 0 && keplerDocumentationAttribute != null) {
150            //if there is both a docAttribute and a KeplerDocumentationAttribute
151            //use the preference passed in to the constructor
152            if (_docPreference == 0) {
153                keplerDocumentationAttribute = null;
154            } else if (_docPreference == 1) {
155                docAttributeSize = 0;
156            }
157        }
158
159        if (keplerDocumentationAttribute != null) {
160            //use the KeplerDocumentationAttribute
161            DocAttribute docAttribute = keplerDocumentationAttribute
162                    .getDocAttribute(target);
163            if (docAttribute != null) {
164                _showDocAttributeTableau(docAttribute, target);
165            } else {
166                throw new InternalErrorException(
167                        "Error building Kepler documentation");
168            }
169        } else if (docAttributeSize != 0) {
170            // Have a doc attribute. Use that.
171            DocAttribute docAttribute = (DocAttribute) docAttributes
172                    .get(docAttributes.size() - 1);
173            _showDocAttributeTableau(docAttribute, target);
174        } else {
175            // No doc attribute. Try for a doc file.
176            String className = target.getClass().getName();
177            Effigy context = Configuration.findEffigy(target);
178            NamedObj container = target.getContainer();
179            while (context == null && container != null) {
180                context = Configuration.findEffigy(container);
181                container = container.getContainer();
182            }
183            /* This test is pointless, since it shows the doc anyway.
184            if (context == null) {
185                MessageHandler.error("Cannot find an effigy for "
186                        + target.getFullName());
187            }
188             */
189            getDocumentation(_configuration, className, context);
190        }
191    }
192
193    /** Get the documentation for a particular class.
194     *  <p>If the configuration has a parameter _docApplicationSpecializer
195     *  and that parameter names a class that that implements the
196     *  DocApplicationSpecializer interface, then we call
197     *  docClassNameToURL().
198     *
199     *  <p>If the documentation is not found, pop up a dialog and ask the
200     *  user if they would like to build the documentation, use the
201     *  website documentation or cancel.  The location of the website
202     *  documentation is set by the _remoteDocumentationURLBase attribute
203     *  in the configuration.  That attribute, if present, should be a
204     *  parameter that whose value is a string that represents the URL
205     *  where the documentation may be found.  If the
206     *  _remoteDocumentationURLBase attribute is not set, then the
207     *  location of the website documentation defaults to
208     *  <code>http://ptolemy.eecs.berkeley.edu/ptolemyII/ptII/<i>Major.Version</i></code>,
209     *  where <code><i>Major.Version</i></code> is the value returned by
210     *  {@link ptolemy.kernel.attributes.VersionAttribute#majorCurrentVersion()}.
211     *
212     *  @param configuration The configuration.
213     *  @param className The dot separated fully qualified name of the class.
214     *  @param context The context.
215     */
216    public static void getDocumentation(Configuration configuration,
217            String className, Effigy context) {
218        try {
219
220            // Look for the PtDoc .xml file or the javadoc.
221            // Don't look for the source or the index.
222            URL toRead = DocManager.docClassNameToURL(configuration, className,
223                    true, true, false, false);
224            if (toRead != null) {
225                _lastClassName = null;
226                if (toRead.toExternalForm().endsWith(".html")) {
227                    // Sadly, Javadoc from Java 1.7 cannot be
228                    // displayed using a JEditorPane, so we open
229                    // javadoc in an external browser.  To test this
230                    // out, see
231                    // http://docs.oracle.com/javase/tutorial/uiswing/components/editorpane.html#editorpane
232                    // and modify the example so that it tries to view
233                    // the Javadoc for Object.
234                    toRead = new URL(toRead.toExternalForm() + "#in_browser");
235                }
236                // Opening a remote URL can be slow, so we report to the status bar.
237                BasicGraphFrame basicGraphFrame = BasicGraphFrame
238                        .getBasicGraphFrame(context);
239
240                if (basicGraphFrame != null) {
241                    basicGraphFrame.report("Opening " + toRead);
242                }
243                configuration.openModel(null, toRead, toRead.toExternalForm());
244                if (basicGraphFrame != null) {
245                    basicGraphFrame
246                            .report("Opened documentation for " + className);
247                }
248            } else {
249                Parameter docApplicationSpecializerParameter = (Parameter) configuration
250                        .getAttribute("_docApplicationSpecializer",
251                                Parameter.class);
252                if (docApplicationSpecializerParameter != null) {
253                    //if there is a docApplicationSpecializer, let it handle the
254                    //error instead of just throwing the exception
255                    String docApplicationSpecializerClassName = docApplicationSpecializerParameter
256                            .getExpression();
257                    Class docApplicationSpecializerClass = Class
258                            .forName(docApplicationSpecializerClassName);
259                    final DocApplicationSpecializer docApplicationSpecializer = (DocApplicationSpecializer) docApplicationSpecializerClass
260                            .newInstance();
261                    docApplicationSpecializer
262                            .handleDocumentationNotFound(className, context);
263                } else {
264                    throw new Exception("Could not get find documentation for "
265                            + className + "."
266                            + (DocManager
267                                    .getRemoteDocumentationURLBase() != null
268                                            ? " Also tried looking on \""
269                                                    + DocManager
270                                                            .getRemoteDocumentationURLBase()
271                                                    + "\"."
272                                            : ""));
273                }
274            }
275        } catch (Exception ex) {
276            // Try to open the DocBuilderGUI
277            try {
278                Parameter remoteDocumentationURLBaseParameter = (Parameter) configuration
279                        .getAttribute("_remoteDocumentationURLBase",
280                                Parameter.class);
281                String tentativeRemoteDocumentationURLBase = null;
282                if (remoteDocumentationURLBaseParameter != null) {
283                    tentativeRemoteDocumentationURLBase = remoteDocumentationURLBaseParameter
284                            .getExpression();
285                } else {
286                    if (VersionAttribute.CURRENT_VERSION.getExpression()
287                            .indexOf(".devel") != -1) {
288                        tentativeRemoteDocumentationURLBase = "https://chess.eecs.berkeley.edu/ptexternal/src/ptII/";
289                    } else {
290                        tentativeRemoteDocumentationURLBase = "http://ptolemy.eecs.berkeley.edu/ptolemyII/ptII"
291                                + VersionAttribute.majorCurrentVersion()
292                                + "/ptII/";
293                    }
294                }
295                // Pop up a query an prompt the user
296                String message = "The documentation for " + className
297                        + " was not found.\n"
298                        + (_lastClassName != null && DocManager
299                                .getRemoteDocumentationURLBase() != null
300                                        ? " We looked in \"" + DocManager
301                                                .getRemoteDocumentationURLBase()
302                                                + "\" but did not find anything.\n"
303                                        : "")
304                        + "You may\n"
305                        + "1) Build the documentation, which requires "
306                        + "configure and make, or\n"
307                        + "2) Use the documentation from the website at \""
308                        + tentativeRemoteDocumentationURLBase + "\" or\n"
309                        + "3) Cancel";
310                Object[] options = { "Build", "Use Website", "Cancel" };
311                int selected = JOptionPane.showOptionDialog(null, message,
312                        "Choose Documentation Source",
313                        JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE,
314                        null, options, options[0]);
315                switch (selected) {
316                case 2:
317                    // Cancel
318                    return;
319                case 1:
320                    // Use Website
321                    DocManager.setRemoteDocumentationURLBase(
322                            tentativeRemoteDocumentationURLBase);
323                    _lastClassName = className;
324                    getDocumentation(configuration, className, context);
325                    break;
326                case 0:
327                    // Build
328                    // Need to create an effigy and tableau.
329                    ComponentEntity effigy = context
330                            .getEntity("DocBuilderEffigy");
331                    if (effigy == null) {
332                        try {
333                            effigy = new DocBuilderEffigy(context,
334                                    "DocBuilderEffigy");
335                        } catch (KernelException exception) {
336                            throw new InternalErrorException(exception);
337                        }
338                    }
339                    if (!(effigy instanceof DocBuilderEffigy)) {
340                        MessageHandler.error("Found an effigy named "
341                                + "DocBuilderEffigy that "
342                                + "is not an instance of DocBuilderEffigy!");
343                    }
344                    //((DocEffigy) effigy).setDocAttribute(docAttribute);
345                    ComponentEntity tableau = ((Effigy) effigy)
346                            .getEntity("DocBuilderTableau");
347                    if (tableau == null) {
348                        try {
349                            tableau = new DocBuilderTableau(
350                                    (DocBuilderEffigy) effigy,
351                                    "DocBuilderTableau");
352                            ((DocBuilderTableau) tableau)
353                                    .setTitle("Documentation for " + className);
354                        } catch (KernelException exception) {
355                            throw new InternalErrorException(exception);
356                        }
357                    }
358                    if (!(tableau instanceof DocBuilderTableau)) {
359                        MessageHandler.error("Found a tableau named "
360                                + "DocBuilderTableau that "
361                                + "is not an instance of DocBuilderTableau!");
362                    }
363                    // FIXME: Tell the user what to do here.
364                    ((DocBuilderTableau) tableau).show();
365                    break;
366                default:
367                    throw new InternalErrorException("Unknown return value \""
368                            + selected
369                            + "\" from Choose Documentation Source window.");
370                    //break;
371                }
372            } catch (Throwable throwable) {
373                MessageHandler.error("Cannot find documentation for "
374                        + className + "\nTry Running \"make\" in ptII/doc."
375                        + "\nor installing the documentation component.",
376                        throwable);
377            }
378        }
379    }
380
381    /** Set the configuration.  This is used
382     *  to open files (such as documentation).  The configuration is
383     *  is important because it keeps track of which files are already
384     *  open and ensures that there is only one editor operating on the
385     *  file at any one time.
386     *  @param configuration The configuration.
387     */
388    public void setConfiguration(Configuration configuration) {
389        _configuration = configuration;
390    }
391
392    /** Set the effigy to be used if the effigy is not evident from the
393     *  model being edited.  This is used if you are showing the documentation
394     *  from code that is not in a model.
395     *  @param effigy the effigy to set.
396     */
397    public void setEffigy(Effigy effigy) {
398        _effigy = effigy;
399    }
400
401    ///////////////////////////////////////////////////////////////////
402    ////                         protected variables               ////
403
404    /** The configuration. */
405    protected Configuration _configuration;
406
407    ///////////////////////////////////////////////////////////////////
408    ////                         private methods                   ////
409
410    /**
411     * Allow optional use of multiple documentation windows when the
412     * _multipleDocumentationAllowed attribute is found in the
413     * Configuration.
414     */
415    private static boolean _isMultipleDocumentationAllowed() {
416        // FIXME: This is necessary for Kepler, but not for Ptolemy?
417        // Why?
418        boolean retVal = false;
419        List configsList = Configuration.configurations();
420        Configuration config = null;
421
422        for (Iterator it = configsList.iterator(); it.hasNext();) {
423            config = (Configuration) it.next();
424            if (config != null) {
425                break;
426            }
427        }
428        if (config == null) {
429            throw new KernelRuntimeException("Could not find "
430                    + "configuration, list of configurations was "
431                    + configsList.size() + " elements, all were null.");
432        }
433        // Look up the attribute (if it exists)
434        StringAttribute multipleDocumentationAllowed = (StringAttribute) config
435                .getAttribute("_multipleDocumentationAllowed");
436        if (multipleDocumentationAllowed != null) {
437            retVal = Boolean
438                    .parseBoolean(multipleDocumentationAllowed.getExpression());
439        }
440        return retVal;
441    }
442
443    /**
444     * Find and show the tableau for a given DocAttribute.
445     * @param docAttribute the attribute to show
446     * @param target the target of the documentation viewing
447     */
448    private void _showDocAttributeTableau(DocAttribute docAttribute,
449            NamedObj target) {
450        // Need to create an effigy and tableau.
451        ComponentEntity effigy = null;
452        Effigy context = Configuration.findEffigy(target);
453        if (_effigy == null) {
454            NamedObj container = target.getContainer();
455            while (container != null && context == null) {
456                context = Configuration.findEffigy(container);
457                container = container.getContainer();
458            }
459            if (context == null) {
460                MessageHandler.error(
461                        "Cannot find an effigy for " + target.getFullName());
462                return;
463            }
464            effigy = context.getEntity("DocEffigy");
465        } else {
466            effigy = _effigy;
467        }
468
469        if (effigy == null) {
470            try {
471                effigy = new DocEffigy(context, "DocEffigy");
472            } catch (KernelException exception) {
473                throw new InternalErrorException(exception);
474            }
475        }
476        if (!(effigy instanceof DocEffigy)) {
477            MessageHandler.error("Found an effigy named DocEffigy that "
478                    + "is not an instance of DocEffigy!");
479        }
480        ((DocEffigy) effigy).setDocAttribute(docAttribute);
481        ComponentEntity tableau = ((Effigy) effigy).getEntity("DocTableau");
482        if (tableau == null) {
483            try {
484                tableau = new DocTableau((DocEffigy) effigy, "DocTableau");
485
486                ((DocTableau) tableau)
487                        .setTitle("Documentation for " + target.getFullName());
488            } catch (KernelException exception) {
489                throw new InternalErrorException(exception);
490            }
491        } else {
492            if (_isMultipleDocumentationAllowed() ||
493            // For some reason, the frame might be null.
494                    (tableau instanceof Tableau)
495                            && ((Tableau) tableau).getFrame() == null) {
496                try {
497                    // FIXME: This is necessary for Kepler, but
498                    // not for Ptolemy?  Why?
499
500                    // Create a new tableau with a unique name
501                    tableau = new DocTableau((DocEffigy) effigy,
502                            effigy.uniqueName("DocTableau"));
503                    ((DocTableau) tableau).setTitle(
504                            "Documentation for " + target.getFullName());
505                } catch (KernelException exception) {
506                    MessageHandler.error("Failed to display documentation for "
507                            + "\" " + target.getFullName() + "\".", exception);
508                }
509            }
510        }
511        if (!(tableau instanceof DocTableau)) {
512            MessageHandler.error("Found a tableau named DocTableau that "
513                    + "is not an instance of DocTableau!");
514        }
515        ((DocTableau) tableau).show();
516    }
517
518    ///////////////////////////////////////////////////////////////////
519    ////                         private variables                 ////
520
521    /**
522     * Defines a preference for whether to display kepler documentation or
523     * ptolemy documentation.  This can be set in the constructor and it
524     * default to ptolemy.  0 is ptolemy, 1 is kepler.
525     */
526    private int _docPreference = 0;
527
528    /**
529     * Defines the effigy to use if the effigy is not apparent from the model
530     */
531    private Effigy _effigy = null;
532
533    /** The name of the last class for which we looked.  If the user
534     *  looks again for the same class and gets an error and
535     *  remoteDocumentationURLBase is set, we print a little more information.
536     */
537    private static String _lastClassName = null;
538}