001/* Utilities used to manipulate classes
002
003 Copyright (c) 2003-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
030import java.io.File;
031import java.io.IOException;
032import java.net.JarURLConnection;
033import java.net.URL;
034import java.util.Enumeration;
035import java.util.LinkedList;
036import java.util.List;
037import java.util.jar.JarEntry;
038import java.util.jar.JarFile;
039
040///////////////////////////////////////////////////////////////////
041//// ClassUtilities
042
043/**
044 A collection of utilities for manipulating classes.
045 These utilities do not depend on any other ptolemy.* packages.
046
047
048 @author Christopher Hylands
049 @version $Id$
050 @since Ptolemy II 4.0
051 @Pt.ProposedRating Green (cxh)
052 @Pt.AcceptedRating Green (cxh)
053 */
054public class ClassUtilities {
055    /** Instances of this class cannot be created.
056     */
057    private ClassUtilities() {
058    }
059
060    ///////////////////////////////////////////////////////////////////
061    ////                         public methods                    ////
062
063    /** Return the directories in a jar URI, relative to the jar entry,
064     *  if any.   .
065     *  Jar URLS have several forms, the most common being:
066     *  <code>jar:file:///foo/bar.jar/!/bif/baz</code>, which means that
067     *  the jar file /foo/bar.jar has a directory or file name bif/baz.
068     *  If such a file is passed to this method, then any directories
069     *  in the jar file directory bif/baz will be returned.
070     *  @param jarURL The Jar URL for which we are to look for directories.
071     *  @return An list of Strings that name the directories
072     *  @exception IOException If opening the connection fails or if
073     *  getting the jar file from the connection fails
074     */
075    public static List jarURLDirectories(URL jarURL) throws IOException {
076        List directories = new LinkedList();
077        JarURLConnection connection = (JarURLConnection) jarURL
078                .openConnection();
079        String jarEntryName = connection.getEntryName();
080        if (jarEntryName.endsWith("/")) {
081            jarEntryName = jarEntryName.substring(0, jarEntryName.length() - 1);
082        }
083        JarFile jarFile = connection.getJarFile();
084        Enumeration entries = jarFile.entries();
085        while (entries.hasMoreElements()) {
086            JarEntry entry = (JarEntry) entries.nextElement();
087            String name = entry.getName();
088            int jarEntryIndex = name.indexOf(jarEntryName + "/");
089            int jarEntrySlashIndex = jarEntryIndex + jarEntryName.length() + 1;
090
091            int nextSlashIndex = name.indexOf("/", jarEntrySlashIndex);
092            int lastSlashIndex = name.indexOf("/", jarEntrySlashIndex);
093
094            if (jarEntryIndex > -1 && jarEntrySlashIndex > -1
095                    && nextSlashIndex > -1 && nextSlashIndex == lastSlashIndex
096                    && nextSlashIndex == name.length() - 1
097                    && entry.isDirectory()) {
098                directories.add(name);
099            }
100        }
101        return directories;
102    }
103
104    /** Lookup a jar URL and return the resource.
105
106     *  A resource is a file such as a class file or image file that
107     *  is found in the classpath.  A jar URL is a URL that refers to
108     *  a resource in a jar file.  For example,
109     *  <code>file://./foo.jar!/a/b/c.class</code> is a jar URL that
110     *  refers to the <code>a/b/c.class</code> resource in
111     *  <code>foo.jar</code>.  If this method is called with
112     *  <code>file://./foo.jar!/a/b/c.class</code> then it will return
113     *  <code>a/b/c.class</code> if <code>a/b/c.class</code> can be
114     *  found as a resource in the class loader that loaded this class
115     *  (ptolemy.util.ClassUtilities).  If the resource cannot be found,
116     *  then an IOException is thrown. If the jarURLString parameter
117     *  does not contain <code>!/</code>, then return null.
118     *  Note that everything before the <code>!/</code> is removed before
119     *  searching the classpath.
120     *
121     *  <p>This method is necessary because Web Start uses jar URL, and
122     *  there are some cases where if we have a jar URL, then we may
123     *  need to strip off the jar:<i>url</i>!/ part so that we can
124     *  search for the {entry} as a resource.
125     *
126     *  @param jarURLString The string containing the jar URL.
127     *  If no resource is found and the string contains a "#" then the text
128     *  consisting of the # and the remaining text is removed and the shorter
129     *  string is used as a search pattern.
130     *  @return The resource, if any.  If the spec string does not
131     *  contain <code>!/</code>, then return null.
132     *  @exception IOException If this method cannot convert the specification
133     *  to a URL.
134     *  @see java.net.JarURLConnection
135     */
136    public static URL jarURLEntryResource(String jarURLString)
137            throws IOException {
138        // At first glance, it would appear that this method could appear
139        // in specToURL(), but the problem is that specToURL() creates
140        // a new URL with the spec, so it only does further checks if
141        // the URL is malformed.  Unfortunately, in Web Start applications
142        // the URL will often refer to a resource in another jar file,
143        // which means that the jar url is not malformed, but there is
144        // no resource by that name.  Probably specToURL() should return
145        // the resource after calling new URL().
146        int jarEntry = jarURLString.indexOf("!/");
147
148        if (jarEntry == -1) {
149            jarEntry = jarURLString.indexOf("!\\");
150
151            if (jarEntry == -1) {
152                return null;
153            }
154        }
155
156        try {
157            // !/ means that this could be in a jar file.
158            String entry = jarURLString.substring(jarEntry + 2);
159
160            // We might be in the Swing Event thread, so
161            // Thread.currentThread().getContextClassLoader()
162            // .getResource(entry) probably will not work.
163            Class refClass = Class.forName("ptolemy.util.ClassUtilities");
164            URL entryURL = refClass.getClassLoader().getResource(entry);
165            if (entryURL == null && entry.indexOf("#") != -1) {
166                // If entry contains a #, then strip it off and try again.
167                entryURL = refClass.getClassLoader()
168                        .getResource(entry.substring(0, entry.indexOf("#")));
169            }
170            return entryURL;
171        } catch (Exception ex) {
172            // IOException constructor does not take a cause, so we add it.
173            IOException ioException = new IOException(
174                    "Cannot find \"" + jarURLString + "\".");
175            ioException.initCause(ex);
176            throw ioException;
177        }
178    }
179
180    /** Lookup a URL in the classpath, but search up the classpath
181     *  for directories named src.
182     *  This method is useful for IDEs such as Eclipse where
183     *  the default is to place .class files in a separate directory
184     *  from the .java files.
185     *
186     *  <p>Before calling this method, call ClassLoader.getResource()
187     *  because ClassLoader.getResource() is presumably faster.</p>
188     *
189     *  @param sourceURLString The string containing the URL.
190     *  @return The resource, if any.  If the spec string does not
191     *  contain <code>!/</code>, then return null.
192     *  @exception IOException If this method cannot convert the specification
193     *  to a URL.
194     *  @see java.net.JarURLConnection
195     */
196    public static URL sourceResource(String sourceURLString)
197            throws IOException {
198        // FIXME: Maybe only allow relative paths?
199
200        // Hmm.  Might be Eclipse, where sadly the
201        // .class files are often in a separate directory
202        // than the .java files.  So, we look at the CLASSPATH
203        // and for each element that names a directory, traverse
204        // the parents directories and look for adjacent directories
205        // that contain a "src" directory.  For example if
206        // the classpath contains "kepler/ptolemy/target/classes/",
207        // then we will find kepler/ptolemy/src and return it
208        // as a URL.
209
210        // First time through, search each element of the CLASSPATH that names
211        // a directory
212        if (_sourceDirectories == null) {
213            List<File> sourceDirectories = new LinkedList<File>();
214            String classPath[] = StringUtilities.getProperty("java.class.path")
215                    .split(StringUtilities.getProperty("path.separator"));
216            for (String element : classPath) {
217                File directory = new File(element);
218                if (directory.isDirectory()) {
219                    // We have a potential winner.
220                    while (directory != null) {
221                        File sourceDirectory = new File(directory, "src");
222                        if (sourceDirectory.isDirectory()) {
223                            sourceDirectories.add(sourceDirectory);
224                            break;
225                        }
226                        directory = directory.getParentFile();
227                    }
228                }
229            }
230            // Avoid FindBugs: LI: Incorrect lazy initialization and update of static field.
231            _sourceDirectories = sourceDirectories;
232        }
233
234        // Search _sourceDirectories for sourceURLString
235        for (File sourceDirectory : _sourceDirectories) {
236            File sourceFile = new File(sourceDirectory, sourceURLString);
237            if (sourceFile.exists()) {
238                return sourceFile.getCanonicalFile().toURI().toURL();
239            }
240        }
241        return null;
242    }
243
244    /** Get the resource.
245     *  If the current thread has a non-null context class loader,
246     *  then use it to get the resource.  Othewise, get the
247     *  NamedObj class and use that to get the resource.
248     *  This is necessary because Thread.currentThread() can return null.
249     *  @param spec The string to be found as a resource.
250     *  @return The URL
251     */
252    public static URL getResource(String spec) {
253        URL url = null;
254        // Unfortunately, Thread.currentThread().getContextClassLoader() can
255        // return null.  This happened when
256        // $CLASSPATH/ptolemy/actor/lib/vertx/demo/TokenTransmissionTime/Sender.xml
257        // failed and subsequent calls to ConfigurationApplication.openModelOrEntity() would
258        // fail because the configuration could not be found.
259        // So, we have our own getResource() that handles this.
260
261        ClassLoader classLoader = Thread.currentThread()
262                .getContextClassLoader();
263        if (classLoader != null) {
264            url = classLoader.getResource(spec);
265        } else {
266            classLoader = ClassLoader.getSystemClassLoader();
267            if (classLoader != null) {
268                url = classLoader.getResource(spec);
269            } else {
270                try {
271                    Class refClass = Class
272                            .forName("ptolemy.util.ClassUtilities");
273                    url = refClass.getClassLoader().getResource(spec);
274                } catch (Exception ex) {
275                    throw new RuntimeException(
276                            "Failed to get system class loader"
277                                    + " and failed to get the Class for ClassUtilities",
278                            ex);
279                }
280            }
281        }
282        return url;
283    }
284
285    /** Given a dot separated classname, return the jar file or directory
286     *  where the class can be found.
287     *  @param necessaryClass  The dot separated class name, for example
288     *  "ptolemy.util.ClassUtilities"
289     *  @return If the class can be found as a resource, return the
290     *  directory or jar file where the necessary class can be found.
291     *  otherwise, return null.  If the resource is found in a directory,
292     *  then the return value will always have forward slashes, it will
293     *  never use backslashes.
294     */
295    public static String lookupClassAsResource(String necessaryClass) {
296        // This method is called from copernicus.kernel.GeneratorAttribute
297        // and actor.lib.python.PythonScript.  We moved it here
298        // to avoid dependencies.
299        String necessaryResource = StringUtilities.substitute(necessaryClass,
300                ".", "/") + ".class";
301
302        URL necessaryURL = ClassUtilities.getResource(necessaryResource);
303
304        if (necessaryURL == null) {
305            necessaryResource = StringUtilities.substitute(necessaryClass, ".",
306                    "/") + ".xml";
307            necessaryURL = ClassUtilities.getResource(necessaryResource);
308        }
309        if (necessaryURL != null) {
310            String resourceResults = necessaryURL.getFile();
311
312            // Strip off the file:/ and the necessaryResource.
313            if (resourceResults.startsWith("file:/")) {
314                resourceResults = resourceResults.substring(6);
315            }
316
317            // Strip off the name of the resource we were looking for
318            // so that we are left with the directory or jar file
319            // it is in
320            resourceResults = resourceResults.substring(0,
321                    resourceResults.length() - necessaryResource.length());
322
323            // Strip off the trailing !/
324            if (resourceResults.endsWith("!/")) {
325                resourceResults = resourceResults.substring(0,
326                        resourceResults.length() - 2);
327            }
328
329            // Unfortunately, under Windows, URL.getFile() may
330            // return things like /c:/ptII, so we create a new
331            // File and get its path, which will return c:\ptII
332            File resourceFile = new File(resourceResults);
333
334            // Convert backslashes
335            String sanitizedResourceName = StringUtilities
336                    .substitute(resourceFile.getPath(), "\\", "/");
337            return sanitizedResourceName;
338        }
339
340        return null;
341    }
342
343    /** A list of directories that end with "src" that are found in
344     *  in the paths of individual elements in the classpath.
345     */
346    private static List<File> _sourceDirectories = null;
347}