001/* Utilities used to manipulate strings.
002
003 Copyright (c) 2002-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
027 */
028package ptolemy.util;
029
030// Note that classes in ptolemy.util do not depend on any
031// other ptolemy packages.
032import java.io.BufferedReader;
033import java.io.File;
034import java.io.IOException;
035import java.io.StreamTokenizer;
036import java.io.StringReader;
037import java.lang.reflect.Field;
038import java.net.MalformedURLException;
039import java.net.URI;
040import java.net.URL;
041import java.util.ArrayList;
042import java.util.Arrays;
043import java.util.LinkedList;
044import java.util.List;
045import java.util.Properties;
046import java.util.StringTokenizer;
047
048import sun.misc.Unsafe;
049
050///////////////////////////////////////////////////////////////////
051//// StringUtilities
052
053/**
054 A collection of utilities for manipulating strings.
055 These utilities do not depend on any other ptolemy packages.
056
057 @author Christopher Brooks, Contributors: Teale Fristoe
058 @version $Id$
059 @since Ptolemy II 2.1
060 @Pt.ProposedRating Green (eal)
061 @Pt.AcceptedRating Green (cxh)
062 */
063public class StringUtilities {
064    /** Instances of this class cannot be created.
065     */
066    private StringUtilities() {
067    }
068
069    ///////////////////////////////////////////////////////////////////
070    ////                         public methods                    ////
071
072    /** Abbreviate a string.
073     *  If the string is longer than 80 characters, truncate it by
074     *  displaying the first 37 chars, then ". . .", then the last 38
075     *  characters.
076     *  If the <i>longName</i> argument is null, then the string
077     *  "&gt;Unnamed&lt;" is returned.
078     *  @param longName The string to be abbreviated.
079     *  @return The possibly abbreviated name.
080     *  @see #split(String)
081     */
082    public static String abbreviate(String longName) {
083        // This method is used to abbreviate window titles so that long
084        // file names may appear in the window title bar.  It is not
085        // parameterized so that we can force a unified look and feel.
086        // FIXME: it would be nice to split on a nearby space.
087        if (longName == null) {
088            return "<Unnamed>";
089        }
090
091        if (longName.length() <= 80) {
092            return longName;
093        }
094
095        return longName.substring(0, 37) + ". . ."
096                + longName.substring(longName.length() - 38);
097    }
098
099    /** Add a directory to the java.library.path directory..
100     *  The java.library.path directory determines where the JVM
101     *  looks for native shared libraries.  It is typically read once
102     *  when the JVM is started and no longer read after that.
103     *  <p>This code may only work on certain JVMs</p>
104     *
105     *  <p>Based on code from http://forums.sun.com/thread.jspa?threadID=707176
106     *  and http://stackoverflow.com/questions/5419039/is-djava-library-path-equivalent-to-system-setpropertyjava-library-path</p>
107     *
108     *  @param directoryName The directory to be added.
109     *  @exception IOException If there are insufficient permissions to
110     *  get set the java.library.path environment variable or there
111     *  is no such field as usr_paths in the ClassLoader.
112     */
113    public static void addDirectoryToJavaLibraryPath(String directoryName)
114            throws IOException {
115        try {
116            // Java 1.9 throws a warning unless we use this:
117            // https://stackoverflow.com/a/46458447
118            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
119            theUnsafe.setAccessible(true);
120            Unsafe unsafe = (Unsafe) theUnsafe.get(null);
121
122            Field usrPathsField = ClassLoader.class
123                    .getDeclaredField("usr_paths");
124            String[] libraryPathsArray = (String[]) unsafe.getObjectVolatile(
125                    ClassLoader.class, unsafe.staticFieldOffset(usrPathsField));
126            ArrayList<String> libraryPaths = new ArrayList<String>(
127                    Arrays.asList(libraryPathsArray));
128            if (libraryPaths.contains(directoryName)) {
129                return;
130            }
131            libraryPaths.add(directoryName);
132            unsafe.putObjectVolatile(ClassLoader.class,
133                    unsafe.staticFieldOffset(usrPathsField),
134                    libraryPaths.toArray(new String[libraryPaths.size()]));
135            System.setProperty("java.library.path",
136                    System.getProperty("java.library.path") + File.pathSeparator
137                            + directoryName);
138        } catch (IllegalAccessException ex) {
139            IOException ioException = new IOException(
140                    "Failed to get permissions to set library path");
141            ioException.initCause(ex);
142            throw ioException;
143        } catch (NoSuchFieldException ex2) {
144            IOException ioException = new IOException(
145                    "Failed to get field handle to set library path");
146            ioException.initCause(ex2);
147            throw ioException;
148        }
149    }
150
151    /** Add the $PTII/lib directory to the java.library.path directory.
152     *  The java.library.path directory determines where the JVM
153     *  looks for native shared libraries.  It is typically read once
154     *  when the JVM is started and no longer read after that.
155     *  <p>This code may only work on certain JVMs</p>
156     *
157     *  @exception IOException If there are insufficient permissions to
158     *  get set the java.library.path environment variable or there
159     *  is no such field as usr_paths in the ClassLoader.
160     */
161    public static void addPtolemyLibraryDirectoryToJavaLibraryPath()
162            throws IOException {
163        String ptIIProperty = "ptolemy.ptII.dir";
164        String ptII = StringUtilities.getProperty(ptIIProperty);
165        if (ptII.length() > 0) {
166            StringUtilities.addDirectoryToJavaLibraryPath(
167                    ptII + File.separator + "lib");
168        } else {
169            System.err.println(
170                    "Warning: StringUtilities.addPtolemyLibraryDirectory() "
171                            + "could not get the value of the " + ptIIProperty
172                            + ".  This means that loading shared libraries like the Serial I/O "
173                            + "interface could fail. ");
174        }
175    }
176
177    /** Return a string with a maximum line length of <i>length</i>
178     *  characters, limited to the given number of characters.
179     *  If there are more than 10 newlines, then the string is truncated
180     *  after 10 lines.
181     *  If the string is truncated, an ellipsis (three periods in a
182     *  row: "...") will be appended to the end of the string.
183     *  Lines that are longer than 160 characters are split into lines
184     *  that are shorter than 160 characters.
185     *  @param string The string to truncate.
186     *  @param length The number of characters to which to truncate the string.
187     *  @return The possibly truncated string with ellipsis possibly added.
188     */
189    public static String ellipsis(String string, int length) {
190        // If necessary, insert newlines into long strings.
191        // If we don't do split long lines and we throw an exception
192        // with a very long line, then the window close button and
193        // possible the dismiss button will be off the right side of
194        // the screen.
195        // The number 160 was generated by trying different sizes and
196        // seeing what fits on a 1024 wide screen.
197        string = StringUtilities.split(string, 160);
198
199        // Third argument being true means return the delimiters as tokens.
200        StringTokenizer tokenizer = new StringTokenizer(string, LINE_SEPARATOR,
201                true);
202
203        // If there are more than 42 lines and 42 newlines, return
204        // truncate after the first 42 lines and newlines.
205        // This is necessary so that we can deal with very long lines
206        // of text without spaces.
207        if (tokenizer.countTokens() > 42) {
208            StringBuffer results = new StringBuffer();
209
210            for (int i = 0; i < 42 && tokenizer.hasMoreTokens(); i++) {
211                results.append(tokenizer.nextToken());
212            }
213
214            results.append("...");
215            string = results.toString();
216        }
217
218        if (string.length() > length) {
219            return string.substring(0, length - 3) + "...";
220        }
221
222        return string;
223    }
224
225    /** Given a string, replace all the instances of XML special characters
226     *  with their corresponding XML entities.  This is necessary to
227     *  allow arbitrary strings to be encoded within XML.
228     *
229     *  <p>In this method, we make the following translations:
230     *  <pre>
231     *  &amp; becomes &amp;amp;
232     *  " becomes &amp;quot;
233     *  &lt; becomes &amp;lt;
234     *  &gt; becomes &amp;gt;
235     *  newline becomes &amp;#10;
236     *  carriage return becomes $amp;#13;
237     *  </pre>
238     *  @see #unescapeForXML(String)
239     *
240     *  @param string The string to escape.
241     *  @return A new string with special characters replaced.
242     */
243    public static String escapeForXML(String string) {
244        return escapeForXML(string, true);
245    }
246
247    /** Given a string, replace all the instances of XML special characters
248     *  with their corresponding XML entities.  This is necessary to
249     *  allow arbitrary strings to be encoded within XML.
250     *
251     *  <p>In this method, we make the following translations:
252     *  <pre>
253     *  &amp; becomes &amp;amp;
254     *  " becomes &amp;quot;
255     *  &lt; becomes &amp;lt;
256     *  &gt; becomes &amp;gt;
257     *  newline becomes &amp;#10;, if requested.
258     *  carriage return becomes $amp;#13;, if requested.
259     *  </pre>
260     *  @see #unescapeForXML(String)
261     *
262     *  @param string The string to escape.
263     *  @param string Whether or not to escape line break characters.
264     *  @return A new string with special characters replaced.
265     */
266    public static String escapeForXML(String string, boolean escapeLinebreaks) {
267        if (string != null) {
268            StringBuffer buffer = new StringBuffer();
269            buffer.ensureCapacity(string.length());
270            for (int i = 0, n = string.length(); i < n; ++i) {
271                char c = string.charAt(i);
272                switch (c) {
273                case '\n':
274                    if (escapeLinebreaks) {
275                        buffer.append("&#10;");
276                    } else {
277                        buffer.append(c);
278                    }
279                    break;
280                case '\r':
281                    if (escapeLinebreaks) {
282                        buffer.append("&#13;");
283                    } else {
284                        buffer.append(c);
285                    }
286                    break;
287                case '"':
288                    buffer.append("&quot;");
289                    break;
290                case '&':
291                    buffer.append("&amp;");
292                    break;
293                case '<':
294                    buffer.append("&lt;");
295                    break;
296                case '>':
297                    buffer.append("&gt;");
298                    break;
299                default:
300                    buffer.append(c);
301                    break;
302                }
303            }
304            string = buffer.toString();
305        }
306        return string;
307    }
308
309    /** Given a string, return a string that when fed to the
310     *  Ptolemy expression parser, will turn into the argument
311     *  string. That is, replace all the instances of backslashes
312     *  with double backslashes, all quotation marks with \",
313     *  etc.
314     *  For example
315     *  <pre>
316     *  x"y becomes x\"y;
317     *  x\"y becomes x\\\"y;
318     *  x\y"z becomes x\\y\"z;
319     *  x\\y\"z becomes x\\\\y\\\"
320     *  </pre>
321     *  Similarly, this method replaces the following characters
322     *  exactly as defined in Java strings: \n, \t, \b, \r, and \f.
323     *  @param string The string to escape.
324     *  @return A new string with that can be put between quotation marks.
325     */
326    public static String escapeString(String string) {
327        // Since the first string is a regular expression, it needs extra escaping.
328        // I have no idea why the extra escaping is needed on the second argument.
329        string = string.replaceAll("\\\\", "\\\\\\\\");
330        string = string.replaceAll("\"", "\\\\\"");
331        string = string.replaceAll("\n", "\\\\n");
332        string = string.replaceAll("\t", "\\\\t");
333        string = string.replaceAll("\b", "\\\\b");
334        string = string.replaceAll("\r", "\\\\r");
335        // Not needed.
336        // string = string.replaceAll("\'", "\\\\'");
337        return string;
338    }
339
340    /** If the ptolemy.ptII.exitAfterWrapup or the
341     *  ptolemy.ptII.doNotExit properties are not set, then call
342     *  System.exit().
343     *  Ptolemy code should call this method instead of directly calling
344     *  System.exit() so that we can test code that would usually exit.
345     *  @param returnValue The return value of this process, where
346     *  non-zero values indicate an error.
347     */
348    public static void exit(int returnValue) {
349        try {
350            if (StringUtilities.getProperty("ptolemy.ptII.doNotExit")
351                    .length() > 0) {
352                return;
353            }
354        } catch (SecurityException ex) {
355            System.out.println("Warning: failed to get property \""
356                    + "ptolemy.ptII.doNotExit\". "
357                    + "(-sandbox always causes this)");
358        }
359
360        try {
361            if (StringUtilities.getProperty("ptolemy.ptII.exitAfterWrapup")
362                    .length() > 0) {
363                throw new RuntimeException("StringUtilities.exit() was called. "
364                        + "Normally, we would "
365                        + "exit here because Manager.exitAfterWrapup() "
366                        + "was called.  However, because the "
367                        + "ptolemy.ptII.exitAfterWrapup property "
368                        + "is set, we throw this exception instead.");
369            }
370        } catch (SecurityException ex) {
371            System.out.println("Warning: failed to get property \""
372                    + "ptolemy.ptII.exitAfterWrapup\". "
373                    + "(-sandbox always causes this)");
374
375        }
376
377        if (!inApplet()) {
378            // Only call System.exit if we are not in an applet.
379            // Non-zero indicates a problem.
380            System.exit(returnValue);
381        }
382    }
383
384    /** Return a number of spaces that is proportional to the argument.
385     *  If the argument is negative or zero, return an empty string.
386     *  @param level The level of indenting represented by the spaces.
387     *  @return A string with zero or more spaces.
388     */
389    public static String getIndentPrefix(int level) {
390        if (level <= 0) {
391            return "";
392        }
393
394        StringBuffer result = new StringBuffer(level * 4);
395
396        for (int i = 0; i < level; i++) {
397            result.append("    ");
398        }
399
400        return result.toString();
401    }
402
403    /** Get the specified property from the environment. An empty
404     *  string is returned if the property named by the "propertyName"
405     *  argument environment variable does not exist, though if
406     *  certain properties are not defined, then we make various
407     *  attempts to determine them and then set them.  See the javadoc
408     *  page for java.util.System.getProperties() for a list of system
409     *  properties.
410
411     *  <p>The following properties are handled specially
412     *  <dl>
413     *  <dt> "ptolemy.ptII.dir"
414     *  <dd> vergil usually sets the ptolemy.ptII.dir property to the
415     *  value of $PTII.  However, if we are running under Web Start,
416     *  then this property might not be set, in which case we look
417     *  for "ptolemy/util/StringUtilities.class" and set the
418     *  property accordingly.
419     *  <dt> "ptolemy.ptII.dirAsURL"
420     *  <dd> Return $PTII as a URL.  For example, if $PTII was c:\ptII,
421     *  then return file:/c:/ptII/.
422     *  <dt> "user.dir"
423     *  <dd> Return the canonical path name to the current working
424     *  directory.  This is necessary because under Windows with
425     *  JDK1.4.1, the System.getProperty() call returns
426     *  <code><b>c</b>:/<i>foo</i></code> whereas most of the other
427     *  methods that operate on path names return
428     *  <code><b>C</b>:/<i>foo</i></code>.
429     *  </dl>
430     *  @param propertyName The name of property.
431     *  @return A String containing the string value of the property.
432     *  If the property is not found, then we return the empty string.
433     */
434    public static String getProperty(String propertyName) {
435        // NOTE: getProperty() will probably fail in applets, which
436        // is why this is in a try block.
437        String property = null;
438
439        try {
440            property = System.getProperty(propertyName);
441            // if (propertyName.equals("ptolemy.ptII.dir")) {
442            //     System.out.println("StringUtilities.getProperty(" + propertyName + "): " + property);
443            // }
444        } catch (SecurityException ex) {
445            if (!propertyName.equals("ptolemy.ptII.dir")) {
446                // Constants.java depends on this when running with
447                // -sandbox.
448                SecurityException security = new SecurityException(
449                        "Could not find '" + propertyName
450                                + "' System property");
451                security.initCause(ex);
452                throw security;
453            }
454        }
455
456        if (propertyName.equals("user.dir")) {
457            try {
458                if (property == null) {
459                    return property;
460                }
461                File userDirFile = new File(property);
462                return userDirFile.getCanonicalPath();
463            } catch (IOException ex) {
464                return property;
465            }
466        }
467
468        // Check for cases where the ptII property starts with
469        // the string "/cygdrive".  This can happen if the property
470        // was set by doing "PTII=`pwd`" under Cygwin bash.
471        //
472        // If the property starts with $JAVAROOT, and the
473        // propertyName is ptolemy.ptII.dir, then don't return
474        // the property yet, instead, refine it.
475        if (property != null && (!propertyName.equals("ptolemy.ptII.dir")
476                && !property.startsWith("$JAVAROOT"))) {
477            if (propertyName.equals("ptolemy.ptII.dir")
478                    && property.startsWith("/cygdrive")
479                    && !_printedCygwinWarning) {
480                // This error only occurs when users build their own,
481                // so it is safe to print to stderr
482                _printedCygwinWarning = true;
483                System.err.println("ptolemy.ptII.dir property = \"" + property
484                        + "\", which contains \"cygdrive\". "
485                        + "This is almost always an error under Cygwin that "
486                        + "is occurs when one does PTII=`pwd`.  Instead, do "
487                        + "PTII=c:/foo/ptII");
488            }
489
490            return property;
491        } else {
492
493            if (propertyName.equals("ptolemy.ptII.dirAsURL")) {
494                // Return $PTII as a URL.  For example, if $PTII was c:\ptII,
495                // then return file:/c:/ptII/
496                File ptIIAsFile = new File(getProperty("ptolemy.ptII.dir"));
497
498                try {
499                    // Convert first to a URI, then to a URL so that we
500                    // properly handle cases where $PTII has spaces in it.
501                    URI ptIIAsURI = ptIIAsFile.toURI();
502                    URL ptIIAsURL = ptIIAsURI.toURL();
503                    return ptIIAsURL.toString();
504                } catch (java.net.MalformedURLException malformed) {
505                    throw new RuntimeException("While trying to find '"
506                            + propertyName + "', could not convert '"
507                            + ptIIAsFile + "' to a URL", malformed);
508                }
509            }
510
511            if (propertyName.equals("ptolemy.ptII.dir")) {
512                if (_ptolemyPtIIDir != null) {
513                    // Return the previously calculated value
514                    // System.out.println("StringUtilities.getProperty(" + propertyName + "): returning previous " + _ptolemyPtIIDir);
515                    return _ptolemyPtIIDir;
516                } else {
517                    String stringUtilitiesPath = "ptolemy/util/StringUtilities.class";
518
519                    // PTII variable was not set
520                    URL namedObjURL = ClassUtilities
521                            .getResource(stringUtilitiesPath);
522
523                    if (namedObjURL != null) {
524                        // Get the file portion of URL
525                        String namedObjFileName = namedObjURL.getFile();
526
527                        // System.out.println("StringUtilities.getProperty(" + propertyName + "): namedObjURL: " + namedObjURL);
528                        // FIXME: How do we get from a URL to a pathname?
529                        if (namedObjFileName.startsWith("file:")) {
530                            if (namedObjFileName.startsWith("file://")
531                                    || namedObjFileName
532                                            .startsWith("file:\\\\")) {
533                                // We get rid of either file:/ or file:\
534                                namedObjFileName = namedObjFileName
535                                        .substring(6);
536                            } else {
537                                // Get rid of file:
538                                namedObjFileName = namedObjFileName
539                                        .substring(5);
540                            }
541                        }
542
543                        String abnormalHome = namedObjFileName.substring(0,
544                                namedObjFileName.length()
545                                        - stringUtilitiesPath.length());
546
547                        // abnormalHome will have values like: "/C:/ptII/"
548                        // which cause no end of trouble, so we construct a File
549                        // and call toString().
550                        _ptolemyPtIIDir = new File(abnormalHome).toString();
551
552                        // If we are running under Web Start, then strip off
553                        // the trailing "!"
554                        if (_ptolemyPtIIDir.endsWith("/!")
555                                || _ptolemyPtIIDir.endsWith("\\!")) {
556                            _ptolemyPtIIDir = _ptolemyPtIIDir.substring(0,
557                                    _ptolemyPtIIDir.length() - 1);
558                        }
559
560                        // Web Start, we might have
561                        // RMptsupport.jar or
562                        // XMptsupport.jar1088483703686
563                        String ptsupportJarName = File.separator + "DMptolemy"
564                                + File.separator + "RMptsupport.jar";
565
566                        if (_ptolemyPtIIDir.endsWith(ptsupportJarName)) {
567                            _ptolemyPtIIDir = _ptolemyPtIIDir.substring(0,
568                                    _ptolemyPtIIDir.length()
569                                            - ptsupportJarName.length());
570                        } else {
571                            ptsupportJarName = "/DMptolemy/XMptsupport.jar";
572
573                            if (_ptolemyPtIIDir
574                                    .lastIndexOf(ptsupportJarName) != -1) {
575                                _ptolemyPtIIDir = _ptolemyPtIIDir.substring(0,
576                                        _ptolemyPtIIDir
577                                                .lastIndexOf(ptsupportJarName));
578                            } else {
579                                // Ptolemy II 6.0.1 under Windows: remove
580                                // "\ptolemy\ptsupport.jar!"
581                                // If we don't do this, then ptolemy.ptII.dir
582                                // is set incorrectly and then links to the javadoc
583                                // files will not be found if the javadoc only
584                                // exists in codeDoc.jar and lib/ptII.properties
585                                // is not present.
586                                ptsupportJarName = File.separator + "ptolemy"
587                                        + File.separator + "ptsupport.jar";
588
589                                if (_ptolemyPtIIDir
590                                        .lastIndexOf(ptsupportJarName) != -1) {
591                                    _ptolemyPtIIDir = _ptolemyPtIIDir.substring(
592                                            0, _ptolemyPtIIDir.lastIndexOf(
593                                                    ptsupportJarName));
594                                }
595                            }
596                        }
597                    }
598
599                    // Convert %20 to spaces because if a URL has %20 in it,
600                    // then we know we have a space, but file names do not
601                    // recognize %20 as being a single space, instead file names
602                    // see %20 as three characters: '%', '2', '0'.
603                    if (_ptolemyPtIIDir != null) {
604                        _ptolemyPtIIDir = StringUtilities
605                                .substitute(_ptolemyPtIIDir, "%20", " ");
606                    }
607                    //*.class files are compiled into classes.dex file; therefore, check for StringUtilities.class fails
608                    //it's OK to set _ptolemyPtIIDir to an empty string on Android
609                    if (_ptolemyPtIIDir == null && System
610                            .getProperty("java.vm.name").equals("Dalvik")) {
611                        _ptolemyPtIIDir = "";
612                    }
613                    if (_ptolemyPtIIDir == null) {
614                        throw new RuntimeException("Could not find "
615                                + "'ptolemy.ptII.dir'" + " property.  "
616                                + "Also tried loading '" + stringUtilitiesPath
617                                + "' as a resource and working from that. "
618                                + "Vergil should be "
619                                + "invoked with -Dptolemy.ptII.dir"
620                                + "=\"$PTII\", "
621                                + "otherwise the following features will not work: "
622                                + "PtinyOS, Ptalon, the Python actor, "
623                                + "actor document, cg code generation and possibly "
624                                + "other features will not work.");
625                    }
626
627                    try {
628                        // Here, we set the property so that future updates
629                        // will get the correct value.
630                        System.setProperty("ptolemy.ptII.dir", _ptolemyPtIIDir);
631                    } catch (SecurityException security) {
632                        // Ignore, we are probably running as an applet or -sandbox
633                    }
634
635                    // System.out.println("StringUtilities.getProperty(" + propertyName + "): returning " + _ptolemyPtIIDir);
636                    return _ptolemyPtIIDir;
637                }
638            }
639
640            // If the property is not set then we return the empty string.
641            //if (property == null) {
642            return "";
643            //}
644        }
645    }
646
647    /** Return true if we are in an applet.
648     *  @return True if we are running in an applet.
649     */
650    public static boolean inApplet() {
651        boolean inApplet = false;
652        try {
653            StringUtilities.getProperty("HOME");
654        } catch (SecurityException ex) {
655            inApplet = true;
656        }
657        return inApplet;
658    }
659
660    /** Test whether a string is a valid Java identifier.
661     *  Section 3.8 of the Java language spec says:
662     *  <blockquote>
663     *  "An identifier is an unlimited-length sequence of Java letters
664     *  and Java digits, the first of which must be a Java letter. An
665     *  identifier cannot have the same spelling (Unicode character
666     *  sequence) as a keyword (3.9), boolean literal (3.10.3), or
667     *  the null literal (3.10.7)."
668     *  </blockquote>
669     *  Java characters are A-Z, a-z, $ and _.
670     *  <p> Characters that are not permitted in a Java identifier are changed
671     *  to underscores.
672     *  This method does not check whether the string is a keyword or literal.
673     *  @param name The name to be checked.
674     *  @return True if the given name is a valid Java identifier, or false otherwise.
675     */
676    public static boolean isValidIdentifier(String name) {
677        char[] nameArray = name.toCharArray();
678        if (nameArray.length == 0) {
679            return false;
680        }
681        if (!Character.isJavaIdentifierStart(nameArray[0])) {
682            return false;
683        }
684        for (int i = 1; i < nameArray.length; i++) {
685            if (!Character.isJavaIdentifierPart(nameArray[i])) {
686                return false;
687            }
688        }
689        return true;
690    }
691
692    /** Merge the properties in lib/ptII.properties with the current
693     *  properties.  lib/ptII.properties is searched for in the
694     *  classpath.  The value of properties listed in
695     *  lib/ptII.properties do not override properties with the same
696     *  name in the current properties.
697     *  @exception IOException If thrown while looking for the
698     *  $CLASSPATH/lib/ptII.properties file.
699     */
700    public static void mergePropertiesFile() throws IOException {
701        Properties systemProperties = System.getProperties();
702        // Fix for
703        // "ptolemy.util.StringUtilities.mergePropertiesFile() deletes
704        // properties" http://bugzilla.ecoinformatics.org/show_bug.cgi?id=3874
705        // It turns out that the problem seems to be that
706        // newProperties.putAll(systemProperties) does not work in Kepler.
707        Properties newProperties = new Properties(systemProperties);
708        String propertyFileName = "$CLASSPATH/lib/ptII.properties";
709
710        URL propertyFileURL = FileUtilities
711                .nameToURL("$CLASSPATH/lib/ptII.properties", null, null);
712
713        if (propertyFileURL == null) {
714            throw new IOException("Could not find " + propertyFileName);
715        }
716
717        newProperties.load(propertyFileURL.openStream());
718
719        System.setProperties(newProperties);
720    }
721
722    /** Return a string representing the name of the file expected to
723     *  contain the source code for the specified object.  This method
724     *  simply replaces "." with "/" and appends ".java" to the class
725     *  name.
726     *  @param object The object.
727     *  @return The expected source file name.
728     */
729    public static String objectToSourceFileName(Object object) {
730        String sourceFileNameBase = object.getClass().getName().replace('.',
731                '/');
732
733        // Inner classes: Get rid of everything past the first $
734        if (sourceFileNameBase.indexOf("$") != -1) {
735            sourceFileNameBase = sourceFileNameBase.substring(0,
736                    sourceFileNameBase.indexOf("$"));
737        }
738
739        return sourceFileNameBase + ".java";
740    }
741
742    /** Return the preferences directory, creating it if necessary.
743     *  If the PTOLEMYII_DOT environment variable is set, then
744     *  that value is used, otherwise the Java user.home property
745     *  is used
746     *
747     *  The PTOLEMYII_DOT environment variable is used to support
748     *  invoking multiple Kepler processes.
749     *
750     *  @return A string naming the preferences directory.  The last
751     *  character of the string will have the file.separator character
752     *  appended.
753     *  @exception IOException If the directory could not be created.
754     *  @see #PREFERENCES_DIRECTORY
755     */
756    public static String preferencesDirectory() throws IOException {
757        String baseDirectory = System.getProperty("user.home");
758        try {
759            if (System.getenv("PTOLEMYII_DOT") != null) {
760                baseDirectory = System.getenv("PTOLEMYII_DOT");
761                if (baseDirectory.endsWith(File.separator)) {
762                    baseDirectory = baseDirectory.substring(0,
763                            baseDirectory.length() - 1);
764                }
765            }
766        } catch (SecurityException ex) {
767            // Ignore, we are probably in an applet.
768        }
769        String preferencesDirectoryName = baseDirectory + File.separator
770                + StringUtilities.PREFERENCES_DIRECTORY + File.separator;
771
772        File preferencesDirectory = new File(preferencesDirectoryName);
773
774        if (!preferencesDirectory.isDirectory()) {
775            if (preferencesDirectory.mkdirs() == false) {
776                throw new IOException("Could not create user preferences "
777                        + "directory '" + preferencesDirectoryName + "'");
778            }
779        }
780
781        return preferencesDirectoryName;
782    }
783
784    /** Return the name of the properties file.
785     *  The properties file is a file of a format suitable for
786     *  java.util.properties.load(InputStream).
787     *  The file is named "ptII.properties" and is found in the
788     *  {@link #PREFERENCES_DIRECTORY} directory that is returned
789     *  by {@link #preferencesDirectory()}.  Typically, this value
790     *  is "$HOME/.ptolemyII/ptII.properties".
791     *  @see #preferencesDirectory()
792     *  @see #PREFERENCES_DIRECTORY
793     *  @return The name of the properties file.
794     *  @exception IOException If {@link #preferencesDirectory()} throws it.
795     */
796    public static String propertiesFileName() throws IOException {
797        return preferencesDirectory() + "ptII.properties";
798    }
799
800    /** Return a LinkedList of the lines in a string that aren't comments.
801     * @param lines A String containing the lines to be separated.
802     * @return A LinkedList of the lines that aren't comments.
803     * @exception IOException If thrown when reading from the input String.
804     */
805    public static LinkedList<String> readLines(String lines)
806            throws IOException {
807        BufferedReader bufferedReader = null;
808        LinkedList<String> returnList = new LinkedList<String>();
809        String line;
810        bufferedReader = new BufferedReader(new StringReader(lines));
811        try {
812            // Read line by line, skipping comments.
813            while ((line = bufferedReader.readLine()) != null) {
814                line = line.trim();
815                if (!(line.length() == 0 || line.startsWith("/*")
816                        || line.startsWith("//"))) {
817                    returnList.add(line);
818                }
819            }
820        } finally {
821            if (bufferedReader != null) {
822                try {
823                    bufferedReader.close();
824                } catch (IOException ex) {
825                    // Ignore
826                    ex.printStackTrace();
827                }
828            }
829        }
830        return returnList;
831    }
832
833    /** Sanitize a String so that it can be used as a Java identifier.
834     *  Section 3.8 of the Java language spec says:
835     *  <blockquote>
836     *  "An identifier is an unlimited-length sequence of Java letters
837     *  and Java digits, the first of which must be a Java letter. An
838     *  identifier cannot have the same spelling (Unicode character
839     *  sequence) as a keyword (3.9), boolean literal (3.10.3), or
840     *  the null literal (3.10.7)."
841     *  </blockquote>
842     *  Java characters are A-Z, a-z, $ and _.
843     *  <p> Characters that are not permitted in a Java identifier are changed
844     *  to underscores.
845     *  This method does not check that the returned string is a
846     *  keyword or literal.
847     *  Note that two different strings can sanitize to the same
848     *  string.
849     *  This method is commonly used during code generation to map the
850     *  name of a ptolemy object to a valid identifier name.
851     *  @param name A string with spaces and other characters that
852     *  cannot be in a Java name.
853     *  @return A String that follows the Java identifier rules.
854     */
855    public static String sanitizeName(String name) {
856        char[] nameArray = name.toCharArray();
857
858        for (int i = 0; i < nameArray.length; i++) {
859            if (!Character.isJavaIdentifierPart(nameArray[i])) {
860                nameArray[i] = '_';
861            }
862        }
863
864        if (nameArray.length == 0) {
865            return "";
866        } else {
867            if (!Character.isJavaIdentifierStart(nameArray[0])) {
868                return "_" + new String(nameArray);
869            } else {
870                return new String(nameArray);
871            }
872        }
873    }
874
875    /**  If the string is longer than 79 characters, split it up by
876     *  adding newlines in all newline delimited substrings
877     *  that are longer than 79 characters.
878     *  If the <i>longName</i> argument is null, then the string
879     *  "&gt;Unnamed&lt;" is returned.
880     *  @see #abbreviate(String)
881     *  @see #split(String, int)
882     *  @param longName The string to optionally split up
883     *  @return Either the original string, or the string with newlines
884     *  inserted.
885     */
886    public static String split(String longName) {
887        return split(longName, 79);
888    }
889
890    /** If the string is longer than <i>length</i> characters,
891     *  split the string up by adding newlines in all
892     *  newline delimited substrings that are longer than <i>length</i>
893     *  characters.
894     *  If the <i>longName</i> argument is null, then the string
895     *  "&gt;Unnamed&lt;" is returned.
896     *  @see #abbreviate(String)
897     *  @see #split(String)
898     *  @param longName The string to optionally split.
899     *  @param length The maximum length of the sequence of characters
900     *  before a newline is inserted.
901     *  @return Either the original string, or the string with newlines
902     *  inserted.
903     */
904    public static String split(String longName, int length) {
905        if (longName == null) {
906            return "<Unnamed>";
907        }
908
909        if (longName.length() <= length) {
910            return longName;
911        }
912
913        StringBuffer results = new StringBuffer();
914
915        // The third argument is true, which means return the delimiters
916        // as part of the tokens.
917        StringTokenizer tokenizer = new StringTokenizer(longName,
918                LINE_SEPARATOR, true);
919
920        while (tokenizer.hasMoreTokens()) {
921            String token = tokenizer.nextToken();
922            int mark = 0;
923
924            while (mark < token.length() - length) {
925                // We look for the space from the end of the first length
926                // characters.  If we find one, then we use that
927                // as the place to insert a newline.
928                int lastSpaceIndex = token.substring(mark, mark + length)
929                        .lastIndexOf(" ");
930
931                if (lastSpaceIndex < 0) {
932                    // No space found, just insert a new line after length
933                    results.append(token.substring(mark, mark + length)
934                            + LINE_SEPARATOR);
935                    mark += length;
936                } else {
937                    results.append(token.substring(mark, mark + lastSpaceIndex)
938                            + LINE_SEPARATOR);
939                    mark += lastSpaceIndex + 1;
940                }
941            }
942
943            results.append(token.substring(mark));
944        }
945
946        return results.toString();
947    }
948
949    /** Given a file or URL name, return as a URL.  If the file name
950     *  is relative, then it is interpreted as being relative to the
951     *  specified base directory. If the name begins with
952     *  "xxxxxxCLASSPATHxxxxxx" or "$CLASSPATH"
953     *  then search for the file relative to the classpath.
954     *  Note that this is the value of the globally defined constant
955     *  $CLASSPATH available in the expression language.
956     *  If no file is found, then throw an exception.
957     *  @param name The name of a file or URL.
958     *  @param baseDirectory The base directory for relative file names,
959     *   or null to specify none.
960     *  @param classLoader The class loader to use to locate system
961     *   resources, or null to use the system class loader.
962     *  @return A URL, or null if no file name or URL has been specified.
963     *  @exception IOException If the file cannot be read, or
964     *   if the file cannot be represented as a URL (e.g. System.in), or
965     *   the name specification cannot be parsed.
966     *  @exception MalformedURLException If the URL is malformed.
967     *  @deprecated Use FileUtilities.nameToURL instead.
968     */
969    @Deprecated
970    public static URL stringToURL(String name, URI baseDirectory,
971            ClassLoader classLoader) throws IOException {
972        return FileUtilities.nameToURL(name, baseDirectory, classLoader);
973    }
974
975    /** Replace all occurrences of <i>pattern</i> in the specified
976     *  string with <i>replacement</i>.  Note that the pattern is NOT
977     *  a regular expression, and that relative to the
978     *  String.replaceAll() method in jdk1.4, this method is extremely
979     *  slow.  This method does not work well with back slashes.
980     *  @param string The string to edit.
981     *  @param pattern The string to replace.
982     *  @param replacement The string to replace it with.
983     *  @return A new string with the specified replacements.
984     */
985    public static String substitute(String string, String pattern,
986            String replacement) {
987        if (string == null) {
988            return null;
989        }
990        int start = string.indexOf(pattern);
991
992        while (start != -1) {
993            StringBuffer buffer = new StringBuffer(string);
994            buffer.delete(start, start + pattern.length());
995            buffer.insert(start, replacement);
996            string = new String(buffer);
997            start = string.indexOf(pattern, start + replacement.length());
998        }
999
1000        return string;
1001    }
1002
1003    /** Perform file prefix substitution.
1004     *  If <i>string</i> starts with <i>prefix</i>, then we return a
1005     *  new string that consists of the value or <i>replacement</i>
1006     *  followed by the value of <i>string</i> with the value of
1007     *  <i>prefix</i> removed.  For example,
1008     *  substituteFilePrefix("c:/ptII", "c:/ptII/ptolemy, "$PTII")
1009     *  will return "$PTII/ptolemy"
1010     *
1011     *  <p>If <i>prefix</i> is not a simple prefix of <i>string</i>, then
1012     *  we use the file system to find the canonical names of the files.
1013     *  For this to work, <i>prefix</i> and <i>string</i> should name
1014     *  files that exist, see java.io.File.getCanonicalFile() for details.
1015     *
1016     *  <p>If <i>prefix</i> is not a prefix of <i>string</i>, then
1017     *  we return <i>string</i>
1018     *
1019     *  @param prefix The prefix string, for example, "c:/ptII".
1020     *  @param string The string to be substituted, for example,
1021     *  "c:/ptII/ptolemy".
1022     *  @param replacement The replacement to be substituted in, for example,
1023     *  "$PTII"
1024     *  @return The possibly substituted string.
1025     */
1026    public static String substituteFilePrefix(String prefix, String string,
1027            String replacement) {
1028        // This method is currently used by $PTII/util/testsuite/auto.tcl
1029        if (string.startsWith(prefix)) {
1030            // Hmm, what about file separators?
1031            return replacement + string.substring(prefix.length());
1032        } else {
1033            try {
1034                String prefixCanonicalPath = new File(prefix)
1035                        .getCanonicalPath();
1036
1037                String stringCanonicalPath = new File(string)
1038                        .getCanonicalPath();
1039
1040                if (stringCanonicalPath.startsWith(prefixCanonicalPath)) {
1041                    return replacement + stringCanonicalPath
1042                            .substring(prefixCanonicalPath.length());
1043                }
1044            } catch (Throwable throwable) {
1045                // ignore.
1046            }
1047        }
1048
1049        return string;
1050    }
1051
1052    /** Tokenize a String to an array of Strings for use with
1053     *  Runtime.exec(String []).
1054     *
1055     *  <p>Lines that begin with an octothorpe '#' are ignored.
1056     *  Substrings that start and end with a double quote are considered
1057     *  to be a single token and are returned as a single array element.
1058     *
1059     *  @param inputString  The String to tokenize
1060     *  @return An array of substrings.
1061     *  @exception IOException If StreamTokenizer.nextToken() throws it.
1062     */
1063    public static String[] tokenizeForExec(String inputString)
1064            throws IOException {
1065        // The java.lang.Runtime.exec(String command) call uses
1066        // java.util.StringTokenizer() to parse the command string.
1067        // Unfortunately, this means that double quotes are not handled
1068        // in the same way that the shell handles them in that 'ls "foo
1069        // 'bar"' will interpreted as three tokens 'ls', '"foo' and
1070        // 'bar"'.  In the shell, the string would be two tokens 'ls' and
1071        // '"foo bar"'.  What is worse is that the exec() behaviour is
1072        // slightly different under Windows and Unix.  To solve this
1073        // problem, we preprocess the command argument using
1074        // java.io.StreamTokenizer, which converts quoted substrings into
1075        // single tokens.  We then call java.lang.Runtime.exec(String []
1076        // commands);
1077        // Parse the command into tokens
1078        List<String> commandList = new LinkedList<String>();
1079
1080        StreamTokenizer streamTokenizer = new StreamTokenizer(
1081                new StringReader(inputString));
1082
1083        // We reset the syntax so that we don't convert to numbers,
1084        // otherwise, if PTII is "d:\\tmp\\ptII\ 2.0", then
1085        // we have no end of problems.
1086        streamTokenizer.resetSyntax();
1087        streamTokenizer.whitespaceChars(0, 32);
1088        streamTokenizer.wordChars(33, 127);
1089
1090        // We can't use quoteChar here because it does backslash
1091        // substitution, so "c:\ptII" ends up as "c:ptII"
1092        // Substituting forward slashes for backward slashes seems like
1093        // overkill.
1094        // streamTokenizer.quoteChar('"');
1095        streamTokenizer.ordinaryChar('"');
1096
1097        streamTokenizer.eolIsSignificant(true);
1098
1099        streamTokenizer.commentChar('#');
1100
1101        // Current token
1102        String token = "";
1103
1104        // Single character token, usually a -
1105        String singleToken = "";
1106
1107        // Set to true if we are inside a double quoted String.
1108        boolean inDoubleQuotedString = false;
1109
1110        while (streamTokenizer.nextToken() != StreamTokenizer.TT_EOF) {
1111            switch (streamTokenizer.ttype) {
1112            case StreamTokenizer.TT_WORD:
1113
1114                if (inDoubleQuotedString) {
1115                    if (token.length() > 0) {
1116                        // FIXME: multiple spaces will get compacted here
1117                        token += " ";
1118                    }
1119
1120                    token += singleToken + streamTokenizer.sval;
1121                } else {
1122                    token = singleToken + streamTokenizer.sval;
1123                    commandList.add(token);
1124                }
1125
1126                singleToken = "";
1127                break;
1128
1129            case StreamTokenizer.TT_NUMBER:
1130                throw new RuntimeException("Internal error: Found TT_NUMBER: '"
1131                        + streamTokenizer.nval + "'.  We should not be "
1132                        + "tokenizing numbers");
1133
1134                //break;
1135            case StreamTokenizer.TT_EOL:
1136            case StreamTokenizer.TT_EOF:
1137                break;
1138
1139            default:
1140                singleToken = Character.toString((char) streamTokenizer.ttype);
1141
1142                if (singleToken.equals("\"")) {
1143                    if (inDoubleQuotedString) {
1144                        commandList.add(token);
1145                    }
1146
1147                    inDoubleQuotedString = !inDoubleQuotedString;
1148                    singleToken = "";
1149                    token = "";
1150                }
1151
1152                break;
1153            }
1154        }
1155
1156        return commandList.toArray(new String[commandList.size()]);
1157    }
1158
1159    /** Return a string with a maximum line length of <i>lineLength</i>
1160     *  and a maximum number of lines <i>numberOfLines</i>.
1161     *  Each line that exceeds the line length is replaced with a line that
1162     *  ends with "...". If the number of lines exceeds <i>numberOfLines</i>,
1163     *  then the returned string will have exactly <i>numberOfLines</i> lines
1164     *  where the last line is "...".
1165     *  @param string The string to truncate.
1166     *  @param lineLength The number of characters to which to truncate each line.
1167     *  @param numberOfLines The maximum number of lines.
1168     *  @return The possibly truncated string with ellipsis possibly added.
1169     */
1170    public static String truncateString(String string, int lineLength,
1171            int numberOfLines) {
1172
1173        // Third argument being true means the delimiters (LINE_SEPARATOR) are
1174        // included in as tokens in the parsed results.
1175        StringTokenizer tokenizer = new StringTokenizer(string, LINE_SEPARATOR,
1176                true);
1177
1178        StringBuffer results = new StringBuffer();
1179        // Count the lines + newlines.
1180        int lineCount = 0;
1181        while (tokenizer.hasMoreTokens()) {
1182            if (lineCount >= numberOfLines * 2) {
1183                // Presumably, the last line is a line separator.
1184                // We append an additional line to indicate that there
1185                // are more lines.
1186                results.append("...");
1187                break;
1188            }
1189            lineCount++;
1190            String line = tokenizer.nextToken();
1191            if (line.length() > lineLength) {
1192                line = line.substring(0, lineLength - 3) + "...";
1193            }
1194            results.append(line);
1195        }
1196        return results.toString();
1197    }
1198
1199    /** Given a string, replace all the instances of XML entities
1200     *  with their corresponding XML special characters.  This is necessary to
1201     *  allow arbitrary strings to be encoded within XML.
1202     *
1203     *  <p>In this method, we make the following translations:
1204     *  <pre>
1205     *  &amp;amp; becomes &amp;
1206     *  &amp;quot; becomes "
1207     *  &amp;lt; becomes &lt;
1208     *  &amp;gt; becomes &gt;
1209     *  &amp;#10; becomes newline
1210     *  &amp;#13; becomes carriage return
1211     *  </pre>
1212     *  @see #escapeForXML(String)
1213     *
1214     *  @param string The string to escape.
1215     *  @return A new string with special characters replaced.
1216     */
1217    public static String unescapeForXML(String string) {
1218        if (string.indexOf("&") != -1) {
1219            string = substitute(string, "&amp;", "&");
1220            string = substitute(string, "&quot;", "\"");
1221            string = substitute(string, "&lt;", "<");
1222            string = substitute(string, "&gt;", ">");
1223            string = substitute(string, "&#10;", "\n");
1224            string = substitute(string, "&#13;", "\r");
1225        }
1226        return string;
1227    }
1228
1229    /** Return a string that contains a description of how to use a
1230     *  class that calls this method.  For example, this method is
1231     *  called by "$PTII/bin/vergil -help".
1232     *  @param commandTemplate  A string naming the command and the
1233     *  format of the arguments, for example
1234     *  "moml [options] [file . . .]"
1235     *  @param commandOptions A 2xN array of Strings that list command-line
1236     *  options that take arguments where the first
1237     *  element is a String naming the command line option, and the
1238     *  second element is the argument, for example
1239     *  <code>{"-class", "&lt;classname&gt;")</code>
1240     *  @param commandFlags An array of Strings that list command-line
1241     *  options that are either present or not.
1242     *  @return A string that describes the command.
1243     */
1244    public static String usageString(String commandTemplate,
1245            String[][] commandOptions, String[] commandFlags) {
1246        String[][] commandFlagsWithDescriptions = new String[commandFlags.length][2];
1247        for (int i = 0; i < commandFlags.length; i++) {
1248            commandFlagsWithDescriptions[i][0] = commandFlags[i];
1249            commandFlagsWithDescriptions[i][1] = "";
1250        }
1251        return usageString(commandTemplate, commandOptions,
1252                commandFlagsWithDescriptions);
1253    }
1254
1255    /** Return a string that contains a description of how to use a
1256     *  class that calls this method.  For example, this method is
1257     *  called by "$PTII/bin/vergil -help".
1258     *  @param commandTemplate  A string naming the command and the
1259     *  format of the arguments, for example
1260     *  "moml [options] [file . . .]"
1261     *  @param commandOptions A 2xN array of Strings that list command-line
1262     *  options that take arguments where the first
1263     *  element is a String naming the command line option, and the
1264     *  second element is the argument, for example
1265     *  <code>{"-class", "&lt;classname&gt;")</code>
1266     *  @param commandFlagsWithDescriptions A 2xM array of Strings that list
1267     *  command-line options that are either present or not and a description
1268     *  of what the command line option does.
1269     *  @return A string that describes the command.
1270     */
1271    public static String usageString(String commandTemplate,
1272            String[][] commandOptions,
1273            String[][] commandFlagsWithDescriptions) {
1274        // This method is static so that we can reuse it in places
1275        // like copernicus/kernel/Copernicus and actor/gui/MoMLApplication
1276        StringBuffer result = new StringBuffer("Usage: " + commandTemplate
1277                + "\n\n" + "Options that take values:\n");
1278
1279        int i;
1280
1281        for (i = 0; i < commandOptions.length; i++) {
1282            result.append(" " + commandOptions[i][0]);
1283            if (commandOptions[i][1].length() > 0) {
1284                result.append(" " + commandOptions[i][1]);
1285            }
1286            result.append("\n");
1287        }
1288
1289        result.append("\nBoolean flags:\n");
1290
1291        for (i = 0; i < commandFlagsWithDescriptions.length; i++) {
1292            result.append(" " + commandFlagsWithDescriptions[i][0]);
1293            if (commandFlagsWithDescriptions[i][1].length() > 0) {
1294                result.append("\t" + commandFlagsWithDescriptions[i][1]);
1295            }
1296            result.append("\n");
1297        }
1298
1299        return result.toString();
1300    }
1301
1302    ///////////////////////////////////////////////////////////////////
1303    ////                         public variables                  ////
1304    // If you change these, be sure to try running vergil on
1305    // a HSIF moml file
1306    // vergil ../hsif/demo/SwimmingPool/SwimmingPool.xml
1307
1308    /** Maximum length in characters of a long string before
1309     *  {@link #ellipsis(String, int)} truncates and add a
1310     *  trailing ". . .".  This variable is used by callers
1311     *  of ellipsis(String, int).
1312     */
1313    public static final int ELLIPSIS_LENGTH_LONG = 2000;
1314
1315    /** Maximum length in characters of a short string before
1316     *  {@link #ellipsis(String, int)} truncates and add a
1317     *  trailing ". . .". This variable is used by callers
1318     *  of ellipsis(String, int).
1319     */
1320    public static final int ELLIPSIS_LENGTH_SHORT = 400;
1321
1322    /** The line separator string.  Under Windows, this would
1323     *  be "\r\n"; under Unix, "\n"; Under Macintosh, "\r".
1324     */
1325    public static final String LINE_SEPARATOR = System
1326            .getProperty("line.separator");
1327
1328    /** Location of Application preferences such as the user library.
1329     *  This field is not final in case other applications want to
1330     *  set it to a different directory.
1331     *  @see #preferencesDirectory()
1332     */
1333    public static final String PREFERENCES_DIRECTORY = ".ptolemyII";
1334
1335    ///////////////////////////////////////////////////////////////////
1336    ////                         private variables                 ////
1337
1338    /** Set to true if we print the cygwin warning in getProperty(). */
1339    private static boolean _printedCygwinWarning = false;
1340
1341    /** Cached value of ptolemy.ptII.dir property. */
1342    private static String _ptolemyPtIIDir = null;
1343}