001/* A tableau representing an HTML window.
002
003 Copyright (c) 2000-2018 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 */
027package ptolemy.actor.gui;
028
029import java.io.IOException;
030import java.net.MalformedURLException;
031import java.net.URI;
032import java.net.URISyntaxException;
033import java.net.URL;
034
035import ptolemy.kernel.util.Attribute;
036import ptolemy.kernel.util.IllegalActionException;
037import ptolemy.kernel.util.NameDuplicationException;
038import ptolemy.kernel.util.NamedObj;
039import ptolemy.kernel.util.Settable;
040import ptolemy.kernel.util.StringAttribute;
041import ptolemy.util.ClassUtilities;
042import ptolemy.util.FileUtilities;
043import ptolemy.util.StringUtilities;
044
045///////////////////////////////////////////////////////////////////
046//// HTMLViewerTableau
047
048/**
049 A tableau representing a rendered HTML view in a toplevel window.
050 The URL that is viewed is given by the <i>url</i> parameter, and
051 can be either an absolute URL, a system fileName, or a resource that
052 can be loaded relative to the classpath.  For more information about how
053 the URL is specified, see MoMLApplication.specToURL().
054 <p>
055 The constructor of this
056 class creates the window. The text window itself is an instance
057 of HTMLViewer, and can be accessed using the getFrame() method.
058 As with other tableaux, this is an entity that is contained by
059 an effigy of a model.
060 There can be any number of instances of this class in an effigy.
061
062 @author  Steve Neuendorffer and Edward A. Lee
063 @version $Id$
064 @since Ptolemy II 1.0
065 @Pt.ProposedRating Yellow (eal)
066 @Pt.AcceptedRating Red (cxh)
067 @see Effigy
068 @see HTMLViewer
069 @see MoMLApplication#specToURL(String)
070 */
071public class HTMLViewerTableau extends Tableau {
072    /** Construct a new tableau for the model represented by the given effigy.
073     *  This creates an instance of HTMLViewer.  It does not make the frame
074     *  visible.  To do that, call show().
075     *  @param container The container.
076     *  @param name The name.
077     *  @exception IllegalActionException If the container does not accept
078     *   this entity (this should not occur).
079     *  @exception NameDuplicationException If the name coincides with an
080     *   attribute already in the container.
081     */
082    public HTMLViewerTableau(Effigy container, String name)
083            throws IllegalActionException, NameDuplicationException {
084        super(container, name);
085
086        url = new StringAttribute(this, "url");
087        // Set the tableau so that we can get the Configuration and Kepler icon.
088        HTMLViewer frame = new HTMLViewer(this);
089        setFrame(frame);
090        frame.setTableau(this);
091
092    }
093
094    ///////////////////////////////////////////////////////////////////
095    ////                         public parameters                 ////
096
097    /** The URL to display. */
098    public StringAttribute url;
099
100    ///////////////////////////////////////////////////////////////////
101    ////                         public methods                    ////
102
103    /** If the argument is the <i>url</i> parameter, then open the
104     *  specified URL and display its contents.
105     *  @param attribute The attribute that changed.
106     *  @exception IllegalActionException If the URL cannot be opened,
107     *   or if the base class throws it.
108     */
109    @Override
110    public void attributeChanged(Attribute attribute)
111            throws IllegalActionException {
112        if (attribute == url) {
113            String urlSpec = ((Settable) attribute).getExpression();
114
115            try {
116                // NOTE: This cannot handle a URL that is relative to the
117                // MoML file within which this attribute might be being
118                // defined.  Is there any way to do that?
119                URL toRead = ConfigurationApplication.specToURL(urlSpec);
120                ((HTMLViewer) getFrame()).setPage(toRead);
121            } catch (IOException ex) {
122                throw new IllegalActionException(this, ex,
123                        "Cannot open URL: " + urlSpec);
124            }
125        } else {
126            super.attributeChanged(attribute);
127        }
128    }
129
130    ///////////////////////////////////////////////////////////////////
131    ////                         inner classes                     ////
132
133    /** A factory that creates HTML viewer tableaux for Ptolemy models.
134     */
135    public static class Factory extends TableauFactory {
136        /** Create a factory with the given name and container.
137         *  @param container The container.
138         *  @param name The name.
139         *  @exception IllegalActionException If the container is incompatible
140         *   with this attribute.
141         *  @exception NameDuplicationException If the name coincides with
142         *   an attribute already in the container.
143         */
144        public Factory(NamedObj container, String name)
145                throws IllegalActionException, NameDuplicationException {
146            super(container, name);
147        }
148
149        ///////////////////////////////////////////////////////////////////
150        ////                         public methods                    ////
151
152        /** If the specified effigy already contains a tableau named
153         *  "htmlTableau", then return that tableau; otherwise, create
154         *  a new instance of HTMLViewerTableau in the specified
155         *  effigy, and name it "htmlTableau".  If the specified
156         *  effigy is not an instance of HTMLEffigy, then do not
157         *  create a tableau and return null.  It is the
158         *  responsibility of callers of this method to check the
159         *  return value and call show().
160         *  <p>If the URL contains $CLASSPATH, then we look in the
161         *  classpath for the URL.
162         *  @param effigy The effigy.
163         *  @return A HTML viewer tableau, or null if one cannot be
164         *    found or created.
165         *  @exception Exception If the factory should be able to create a
166         *   tableau for the effigy, but something goes wrong.
167         */
168        @Override
169        public Tableau createTableau(Effigy effigy) throws Exception {
170            if (effigy instanceof HTMLEffigy) {
171                // Indicate to the effigy that this factory contains effigies
172                // offering multiple views of the effigy data.
173                effigy.setTableauFactory(this);
174
175                // First see whether the effigy already contains an
176                // HTMLViewerTableau.
177                HTMLViewerTableau tableau = (HTMLViewerTableau) effigy
178                        .getEntity("htmlTableau");
179
180                if (tableau == null) {
181                    tableau = new HTMLViewerTableau(effigy, "htmlTableau");
182                }
183
184                // Unfortunately, if we have a jar url, (for example
185                // jar:file:/C:/foo.jar!/intro.htm
186                // then the java.net.URI toURL() method will return
187                // a URL like jar:, which is missing the file: part
188                // This breaks Ptolemy II under WebStart.
189                URL pageURL = new URL(effigy.uri.getURI().toString());
190
191                try {
192                    ((HTMLViewer) tableau.getFrame()).setPage(pageURL);
193                } catch (IOException io) {
194                    // setPage() throws an IOException if the page can't
195                    // be found.  If we are under Web Start, it could be
196                    // that we are looking in the wrong Jar file, so
197                    // we try again.
198                    String urlString = effigy.uri.getURI().toString();
199                    URL anotherURL = ClassUtilities
200                            .jarURLEntryResource(urlString);
201
202                    if (anotherURL == null && urlString.indexOf("#") != -1) {
203                        anotherURL = _entryResourceWithoutFragment(urlString);
204                    }
205
206                    if (anotherURL == null) {
207                        try {
208                            // Search relative to to $PTII in a jar URL
209                            anotherURL = _absolutePTIIURLToJarURL(urlString);
210                        } catch (Throwable throwable) {
211                            // Ignore: failed
212                        }
213                    }
214
215                    if (anotherURL == null
216                            && urlString.indexOf("$CLASSPATH") != -1) {
217                        // The URL contains $CLASSPATH
218                        String classpathString = urlString
219                                .substring(urlString.indexOf("$CLASSPATH"));
220                        anotherURL = FileUtilities.nameToURL(classpathString,
221                                null, null);
222                    }
223
224                    if (anotherURL == null) {
225                        IOException io2 = new IOException("---");
226                        io2.initCause(io);
227                        throw io2;
228                    }
229
230                    // Try to set the title bar?
231                    try {
232                        effigy.uri.setURI(new URI(anotherURL.toString()));
233                        tableau.setTitle(anotherURL.toString());
234                    } catch (Exception ex) {
235                        try {
236                            // URI's can't deal with spaces, so we
237                            // convert to %20
238                            URL canonicalizedURL = JNLPUtilities
239                                    .canonicalizeJarURL(anotherURL);
240                            effigy.uri.setURI(
241                                    new URI(canonicalizedURL.toString()));
242                            tableau.setTitle(canonicalizedURL.toString());
243                        } catch (Throwable ex2) {
244                            throw ex;
245                        }
246                    }
247
248                    ((HTMLViewer) tableau.getFrame()).setPage(anotherURL);
249                }
250
251                // Don't call show() here.  If show() is called here,
252                // then you can't set the size of the window after
253                // createTableau() returns.  This will affect how
254                // centering works.
255                return tableau;
256            } else {
257                return null;
258            }
259        }
260    }
261
262    ///////////////////////////////////////////////////////////////////
263    ////                         private methods                   ////
264
265    /** If possible convert an absolute URL that refers to a file inside
266     *  the $PTII tree to a jar URL.
267     *  <p>For example, if doc/codeDoc.jar is in the classpath, but
268     *  the contents of codeDoc/ do not exist as files, then calling
269     *  this method with:
270     *  file:/C:/ptII/doc/codeDoc/ptolemy/util/package-summary.html#package_description]
271     *  will return:
272     *  jar:file:/C:/cxh/ptII/doc/codeDoc.jar!/doc/codeDoc/ptolemy/kernel/package-summary.html#package_description
273
274     *
275     *  @param urlName The absolute URL to be converted.
276     *  @return The jar url that refers to a file if the file can be found
277     *  as a resource or null if the file cannot be found.
278     *  @exception URISyntaxException If there are problems creating a URI.
279     *  @exception MalformedURLException If there are problems creating a URL.
280     */
281    public static URL _absolutePTIIURLToJarURL(String urlName)
282            throws java.net.URISyntaxException, java.net.MalformedURLException {
283        // Try looking up the URL as a resource relative to $PTII.
284        String ptIIDirAsURLName = StringUtilities
285                .getProperty("ptolemy.ptII.dirAsURL");
286
287        // FIXME: This is an ugly hack.
288        // If the user has a Windows installation that includes the
289        // source jar file, then when they open whatsNew4.0.htm
290        // and click on a javadoc link that is in codeDoc.jar but
291        // not a separate file, then the file will come up missing
292        // because ptolemy.ptII.dirAsURL refers to ptsupport.jar
293        // The hack is to strip that out.
294        String ptsupportPath = "/ptolemy/ptsupport.jar";
295
296        if (ptIIDirAsURLName.endsWith(ptsupportPath)) {
297            ptIIDirAsURLName = ptIIDirAsURLName.substring(0,
298                    ptIIDirAsURLName.length() - ptsupportPath.length());
299        }
300
301        String relativePath = null;
302
303        if (urlName.startsWith(ptIIDirAsURLName)) {
304            relativePath = urlName.substring(ptIIDirAsURLName.length());
305        } else {
306            // If we click on a link, it might be:
307            // "file:/C:/ptII/doc/codeDoc/"
308            // but ptolemy.ptII.dirAsURL might be
309            // "file:/c:/ptII"
310            // URL.sameFile() will not work here, so we use URI.relativize()
311            URI uri = new URI(urlName);
312            URI ptIIDirAsURI;
313
314            try {
315                ptIIDirAsURI = new URI(ptIIDirAsURLName);
316            } catch (java.net.URISyntaxException ex) {
317                // If the ptIIDirAsURLName has a space in it, then it is
318                // not a legitimate URI, so we substitute in %20
319                ptIIDirAsURI = new URI(StringUtilities
320                        .substitute(ptIIDirAsURLName, " ", "%20"));
321            }
322
323            URI relativeURI = uri.relativize(ptIIDirAsURI);
324
325            if (relativeURI.toURL().sameFile(ptIIDirAsURI.toURL())) {
326                int offset = 0;
327
328                if (urlName.startsWith("jar:")) {
329                    offset = 4;
330                }
331
332                // Hmm, should this be
333                relativePath = uri.toString()
334                        .substring(ptIIDirAsURI.toString().length() + offset);
335
336                //relativePath = urlName.substring(ptIIDirAsURLName.length());
337            }
338        }
339
340        if (relativePath == null) {
341            return null;
342        } else {
343            if (relativePath.startsWith("/")) {
344                relativePath = relativePath.substring(1);
345            }
346
347            URL anotherURL = ClassUtilities.getResource(relativePath);
348
349            if (anotherURL == null && relativePath.indexOf('#') != -1) {
350                // getResource does not work on paths that look like:
351                // "package-summary.html#package_description"
352                // So, we get the resource without the
353                // trailing # and then append it
354                try {
355                    anotherURL = _entryResourceWithoutFragment(relativePath);
356                } catch (IOException ex) {
357                    // Ignored
358                }
359            }
360
361            return anotherURL;
362        }
363    }
364
365    // Given a string that contains a URL that has a # character signifiying
366    // a fragment, strip the fragment off and look up the URL as a resource.
367    // getResource() does not work on paths that look like:
368    // "package-summary.html#package_description"
369    // So, we get the resource without the
370    // trailing # and then append it.  If the resource cannot be found,
371    // we return null
372    // @param urlString A string representing a jar URL or a relative URL.
373    private static URL _entryResourceWithoutFragment(String urlString)
374            throws IOException, MalformedURLException {
375        String urlStringBase = urlString.substring(0,
376                urlString.lastIndexOf("#"));
377
378        URL anotherURL = null;
379
380        if (urlStringBase.startsWith("jar:")) {
381            anotherURL = ClassUtilities.jarURLEntryResource(urlStringBase);
382        } else {
383            anotherURL = ClassUtilities.getResource(urlStringBase);
384        }
385
386        if (anotherURL != null) {
387            anotherURL = new URL(anotherURL.toString()
388                    + urlString.substring(urlString.lastIndexOf("#")));
389        }
390
391        return anotherURL;
392    }
393}