![JAR search and dependency download from the Maven repository](/logo.png)
net.roydesign.mac.MRJAdapter Maven / Gradle / Ivy
Show all versions of mrjadapter Show documentation
/*******************************************************************************
File: MRJAdapter.java
Author: Steve Roy
Part of MRJ Adapter, a unified API for easy integration of Mac OS specific
functionality within your cross-platform Java application.
This library is open source and can be modified and/or distributed under
the terms of the Artistic License.
Change History:
03/11/03 Created this file - Steve
03/18/03 Removed workaround in getFileCreator() for pre-release MRJ 4,
changed getBundleResource() to use MRJFileUtils.getResource()
with MRJ 3 - Steve
03/31/03 Made the cocoaClassLoader member a ClassLoader instead of
URLClassLoader so this works with Java 1.1 code - Steve
06/11/03 Added swingUsesScreenMenuBar() and the set of frameless
menu bar methods - Steve
08/19/03 Added the set of isXXXAutomaticallyPresent() methods,
did some clean up - Steve
08/27/03 Added handling of the new Reopen Application event - Steve
08/31/03 Added the setFileLastModified() method - Steve
09/08/03 Improved parsing of the mrj.version property to better handle
the 1.4.1 update 1 VM, and any future version - Steve
11/25/03 Modified the two getBundleResource() methods to use reflection
with Java 1.3.1 - Steve
01/14/04 Corrected findFolder() to work with MRJ 3.0/3.1 on OS X - Steve
02/17/04 Modified setFramelessMenuBar() to create a JFrame if possible
over a plain AWT frame - Steve
02/26/04 Added awtUsesScreenMenuBar() and added caching of the value
returned by swingUsesScreenMenuBar() - Steve
03/19/04 Added isAppleJDirectAvailable() and getAppleJDirectVersion(),
renamed the xxxUsesScreenMenuBar() methods to the 'is' form,
renamed osTypeStringToInt() to fourCharCodeToInt() and
osTypeIntToString() to intToFourCharCode() - Steve
04/09/04 Added the VERSION constant - Steve
04/15/04 Subclassed invisible JFrame so it's not disposed of, fixed
checkSwingUsingScreenMenuBar() to recognize com.apple.macos.
useScreenMenuBar with Java 1.4.* - Steve
05/26/04 Added openFileResourceFork() with support for MacBinary
Toolkit - Steve
06/29/04 Added support for BrowserLauncher and fixed openURL() not to
hang with Apple's 1.4 VM - Steve
08/10/04 Added calls to setUndecorated() in setFramelessMenuBar() and
setFramelessJMenuBar() so this trick works with 1.4.2_05 - Steve
02/23/05 Removed caching of the result of isSwingUsingScreenMenuBar() so
that it works with runtime changes of the L&F, added explicit
support for Quaqua in isSwingUsingScreenMenuBar() - Steve
03/11/05 Added the set of isAppleXxxVM() methods - Steve
12/05/06 Fixed setFramelessMenuBar() and setFramelessJMenuBar() methods
which left a visible frame with Apple's 1.5 VM - Steve
*******************************************************************************/
package net.roydesign.mac;
import com.apple.eio.FileManager;
import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.LookAndFeel;
import javax.swing.UIManager;
import java.awt.MenuBar;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.stream.Collectors;
/**
* This class offers a unique interface to the functionality provided by
* MRJToolkit with MRJ 1.5, 2.x and 3.x, and com.apple.eawt and com.apple.eio with
* MRJ 4.x. Some functionality is not present in all versions of MRJ, and this
* class makes up for it by implementing itself what was missing, ensuring that
* the methods you use will work on any version of Mac OS, within reason.
*
* The class also contains a collection of methods useful for handling Mac OS
* specific files (like Info.plist and PkgInfo) and converting Mac OS type codes
* between Strings and integers. These methods are written to not depend on any
* Mac OS API to ensure that they can be used on any platform, which could
* happen for example when handling an application bundle that is temporarily
* sitting on a machine other than a Mac.
*
* All methods are designed to behave in a sensible manner when used on
* platforms other than the Mac. Most of the time, this means doing nothing or
* returning the same exception as they do on the Mac when an error occurs.
* Where possible, some methods are functional on all platforms. Check the
* documentation specific to each method to know where they stand. However,
* if you really need cross-platform functionality then you should use the
* higher-level classes in {@code net.roydesign.app} and
* {@code net.roydesign.io} which essentially integrate this functionality
* in a more cross-platform way.
*
* @version MRJ Adapter 1.2
*/
public class MRJAdapter {
/**
* The Cocoa class loader when running on Mac OS X.
*/
private static ClassLoader cocoaClassLoader;
/**
* The name of the startup disk.
*/
private static String startupDisk;
/**
* The path to the current application on disk.
*/
private static String applicationPath;
/**
* The frame that we use to fake the frameless menu bar.
*/
private static JFrame invisibleFrame;
static
{
try
{
cocoaClassLoader = new URLClassLoader(
new URL[] {new URL("file://127.0.0.1/System/Library/Java/")});
}
catch (MalformedURLException ex)
{
// Nothing to worry about since we control the URL string
}
}
/**
* The default constructor is private so this class can't
* be instantiated.
*/
private MRJAdapter()
{
}
/**
* Get the Mac OS creator code of the given file. On other platforms,
* this method returns an empty string, which is the same as if
* the file creator was not set on the Mac platform. A Mac creator is
* a four character string, unless the file has no creator, in which
* case an empty string is returned. Note that this method supports
* Mac OS style bundles on all platforms, which is useful in the
* case where such a bundle is temporarily located on a non-Mac
* file system. When this is detected, the method will extract the
* creator code from the bundle just like Mac OS does natively.
* @param file the file to get to creator from
* @return the creator code
* @exception IOException when an I/O error occurs
*/
public static String getFileCreator(File file) throws IOException
{
if (file.isDirectory())
{
// Check if it's a Mac OS bundle (this should work on any platform)
File contents = new File(file, "Contents");
File infoPlist = new File(contents, "Info.plist");
if (infoPlist.exists())
{
// The Mac OS X Finder apparently gives priority to the
// PkgInfo file, so we do the same thing
File pkgInfo = new File(contents, "PkgInfo");
if (pkgInfo.exists())
{
String t = parsePkgInfo(pkgInfo, "creator");
return t == null ? "" : t;
}
else
{
String t = parseInfoPlist(infoPlist, "CFBundleSignature");
return t == null ? "" : t;
}
}
}
else
{
int c = FileManager.getFileCreator(file.getAbsolutePath());
return intToFourCharCode(c);
}
return "";
}
/**
* Set the date and time of last modification of the given file. MRJ used
* to implement this functionality because the {@code File} class in
* Java 1.1 didn't. This method will use {@code File.setLastModified()}
* when running in Java 1.2 or better, on any platform. When running in
* Java 1.1 on a non-Mac platform, this method will return {@code false}.
* @param file the file to set the creator and type of
* @param time the new modification time, measured in milliseconds since
* the epoch (00:00:00 GMT, January 1, 1970)
* @return whether the operation succeeded or not
*/
public static boolean setFileLastModified(File file, long time)
{
return file.setLastModified(time);
}
/**
* Find a special Mac OS folder. This method locates on disk a folder
* designated by the OS for a specific purpose. For example, this can be
* the Preferences folder or the Desktop folder. Sometimes such folders
* exist in multiple versions for different contexts, which is why the
* method requires a domain to be specified. Most of the time, you will
* want to use the value {@code MRJFolderConstants.kUserDomain}. Then
* you have to specify the type of the folder that you're looking for.
* Each such folder is identified by a unique code. If there is no constant
* defined in {@code MRJFolderConstants} for the folder you're looking
* for then you should use the other form of this method which takes a
* string. This method throws a {@code FileNotFoundException}
* on other platforms, which is the same as what this method does on Mac OS
* when the folder can't be found.
* @param domain the domain of the folder
* @param type the type constant of the folder to find
* @param create whether to create the folder if it doesn't already exist
* @return the special folder object, or null
* @exception FileNotFoundException when the folder can't be found
*/
public static File findFolder(short domain, int type, boolean create)
throws FileNotFoundException
{
return new File(FileManager.findFolder(domain, type, create));
}
/**
* Find a specific Mac OS application. This method locates on disk an
* application given its bundle identifier (e.g. com.apple.Safari) or
* creator code (e.g. R*ch). Note that creator codes are always four
* characters long and case sensitive. The format of the creator code will
* be normalized to be four-characters long if it isn't already. This
* method throws a {@code FileNotFoundException} on other platforms,
* which is the same as what this method does on Mac OS when the folder
* can't be found.
* @param idOrCreator the bundle identifier or creator code of the
* application to find
* @return the application file object
* @exception FileNotFoundException when the application can't be found
*/
public static File findApplication(String idOrCreator) throws IOException
{
/** @todo Can we do this with Cocoa instead (not NSAppleScript)? */
String script = "tell application \"Finder\" to get POSIX path of (application file id \"" +
idOrCreator +
"\" as alias)";
return new File(runAppleScript(script));
}
/**
* Get a resource file from the application bundle. Resource files are
* stored in the folder Contents/Resources inside the bundle. This
* method will locate the requested resource only if it's located at the
* top level of the Resources folder. It returns a {@code File}
* object that can be used to read the resource. This method throws a
* {@code FileNotFoundException} on other platforms, which is the
* same as what this method does on Mac OS when the resource can't be found.
* @param resource the name of the resource file
* @return the resource file
* @exception FileNotFoundException when the resource can't be found
*/
public static File getBundleResource(String resource) throws FileNotFoundException
{ return new File(FileManager.getResource(resource));
}
/**
* Get a resource file from the application bundle. Resource files are
* stored in the folder Contents/Resources inside the bundle. This
* method will locate the requested resource only if it's located in the
* specified subfolder of the Resources folder. It returns a {@code File}
* object that can be used to read the resource. This method throws a
* {@code FileNotFoundException} on other platforms, which is the same
* as what this method does on Mac OS when the resource can't be found.
* @param resource the name of the resource file
* @param subFolder the name of the subfolder of Resources
* @return the resource file
* @exception FileNotFoundException when the resource can't be found
*/
public static File getBundleResource(String resource, String subFolder)
throws FileNotFoundException
{
return new File(FileManager.getResource(resource, subFolder));
}
/**
* Open the Mac OS resource fork of the given file. The resource fork is
* returned as a new unbuffered {@code InputStream}, which can be
* accessed and read in the usual way. This method is functional as is on
* Mac OS X. On classic Mac OS and other platforms, it can only be
* functional if Gregory Guerin's excellent MacBinary Toolkit
* <http://www.amug.org/~glguerin>
* is installed. Note that MacBinary Toolkit will also be used on Mac OS X,
* if it's available. Set useMacBinaryToolkit
to {@code false}
* to turn off this behavior. Finally, resource forks are a feature specific
* to HFS (Mac) file systems, but this method also recognizes a common
* scheme of other systems used as file servers which store resource forks
* in a subdirectory named .HSResource. If the file resource fork doesn't
* exist or can't be opened, the method throws a {@code FileNotFoundException}.
* @param file the file for which to open the resource fork
* @return a new unbuffered input stream to read the resource fork
* @exception FileNotFoundException when the resource fork can't be opened
*/
public static InputStream openFileResourceFork(File file)
throws FileNotFoundException
{
File rf = new File(file, "/..namedfork/rsrc");
if (rf.length() > 0L)
return new FileInputStream(rf);
// Try the HSResource folder trick on any platform
File fo = new File(file.getParent(), ".HSResource");
File hsResource = new File(fo, file.getName());
if (hsResource.exists())
return new FileInputStream(hsResource);
throw new FileNotFoundException();
}
/**
* Open the given URL in the application that is bound to the specified
* protocol. While this method can in theory handle 'file' URLs, the
* {@code DocumentFile.open()} method is preferred. This method is
* functional as is on classic Mac OS and Mac OS X. On other platforms, it
* can only be functional if Eric Albert's excellent BrowserLauncher
* <http://browserlauncher.sourceforge.net>
* is installed. Note that BrowserLauncher will also be used on classic
* Mac OS and Mac OS X, if it's available. Set {@code useBrowserLauncher}
* to {@code false} to turn off this behavior. This method throws an
* {@code IOException} if opening URLs is not supported on the current
* platform, which is the same as what this method does on Mac OS when the
* URL can't be opened.
* @param url the URL string to be opened
* @exception IOException when an I/O error occurs
*/
public static void openURL(String url) throws IOException
{
Runtime.getRuntime().exec(new String[] {"open", url});
}
/**
* Add an About action listener. When the listener is called, the
* {@code ActionEvent} received can be cast to a
* {@code net.roydesign.event.ApplicationEvent}.
* This method does nothing on other platforms.
* @param l the action listener
* @see net.roydesign.event.ApplicationEvent
*/
public static void addAboutListener(ActionListener l)
{
addAboutListener(l, null);
}
/**
* Add an About action listener that receives events from the given source.
* Your application shouldn't normally call this method. Use the single
* parameter variant of this method instead.
* @param l the action listener
* @param source the source to use when firing the event
*/
public static void addAboutListener(ActionListener l, Object source)
{
MRJ4EventProxy.getInstance().addAboutListener(l, source);
}
/**
* Remove an About action listener. This method does nothing on other platforms.
* @param l the action listener
*/
public static void removeAboutListener(ActionListener l)
{
MRJ4EventProxy.getInstance().removeAboutListener(l);
}
/**
* Add a Preferences action listener. When the listener is called, the
* {@code ActionEvent} received can be cast to a
* {@code net.roydesign.event.ApplicationEvent}.
* This method does nothing on other platforms.
* @param l the action listener
* @see net.roydesign.event.ApplicationEvent
*/
public static void addPreferencesListener(ActionListener l)
{
addPreferencesListener(l, null);
}
/**
* Add a Preferences action listener that receives events from the given source.
* Your application shouldn't normally call this method. Use the single
* parameter variant of this method instead.
* @param l the action listener
* @param source the source to use when firing the event
*/
public static void addPreferencesListener(ActionListener l, Object source)
{
MRJ4EventProxy.getInstance().addPreferencesListener(l, source);
}
/**
* Remove a Preferences action listener. This method does nothing on other platforms.
* @param l the action listener
*/
public static void removePreferencesListener(ActionListener l)
{
MRJ4EventProxy.getInstance().removePreferencesListener(l);
}
/**
* Get whether the Preferences menu item is enabled or not. This menu
* item is automatically provided by the OS on Mac OS X. On classic
* Mac OS and other platforms, this method always returns false.
* @return whether the Preferences menu item is enabled
*/
public static boolean isPreferencesEnabled()
{
return MRJ4EventProxy.getInstance().isPreferencesEnabled();
}
/**
* Set whether the Preferences menu item is enabled or not. This menu
* item is automatically provided by the OS on Mac OS X. On classic
* Mac OS and other platforms, this method does nothing.
* @param enabled whether the menu item is enabled
*/
public static void setPreferencesEnabled(boolean enabled)
{
MRJ4EventProxy.getInstance().setPreferencesEnabled(enabled);
}
/**
* Add an Open Application action listener. When the listener is called, the
* {@code ActionEvent} received can be cast to a
* {@code net.roydesign.event.ApplicationEvent}.
* This method does nothing on other platforms.
* @param l the action listener
* @see net.roydesign.event.ApplicationEvent
*/
public static void addOpenApplicationListener(ActionListener l)
{
addOpenApplicationListener(l, null);
}
/**
* Add an Open Application action listener that receives events from the given source.
* Your application shouldn't normally call this method. Use the single
* parameter variant of this method instead.
* @param l the action listener
* @param source the source to use when firing the event
*/
public static void addOpenApplicationListener(ActionListener l, Object source)
{
MRJ4EventProxy.getInstance().addOpenApplicationListener(l, source);
}
/**
* Remove an Open Application action listener. This method does nothing on other platforms.
* @param l the action listener
*/
public static void removeOpenApplicationListener(ActionListener l)
{
MRJ4EventProxy.getInstance().removeOpenApplicationListener(l);
}
/**
* Add a Reopen Application action listener. When the listener is called, the
* {@code ActionEvent} received can be cast to a
* {@code net.roydesign.event.ApplicationEvent}.
* This method does nothing on other platforms.
* @param l the action listener
* @see net.roydesign.event.ApplicationEvent
*/
public static void addReopenApplicationListener(ActionListener l)
{
addReopenApplicationListener(l, null);
}
/**
* Add a Reopen Application action listener that receives events from the given source.
* Your application shouldn't normally call this method. Use the single
* parameter variant of this method instead.
* @param l the action listener
* @param source the source to use when firing the event
*/
public static void addReopenApplicationListener(ActionListener l, Object source)
{
MRJ4EventProxy.getInstance().addReopenApplicationListener(l, source);
}
/**
* Remove a Reopen Application action listener. This method does nothing on other platforms.
* @param l the action listener
*/
public static void removeReopenApplicationListener(ActionListener l)
{
MRJ4EventProxy.getInstance().removeReopenApplicationListener(l);
}
/**
* Add a Quit Application action listener. When the listener is called, the
* {@code ActionEvent} received can be cast to a
* {@code net.roydesign.event.ApplicationEvent}.
* This method does nothing on other platforms.
* @param l the action listener
* @see net.roydesign.event.ApplicationEvent
*/
public static void addQuitApplicationListener(ActionListener l)
{
addQuitApplicationListener(l, null);
}
/**
* Add a Quit Application action listener that receives events from the given source.
* Your application shouldn't normally call this method. Use the single
* parameter variant of this method instead.
* @param l the action listener
* @param source the source to use when firing the event
*/
public static void addQuitApplicationListener(ActionListener l, Object source)
{
MRJ4EventProxy.getInstance().addQuitApplicationListener(l, source);
}
/**
* Remove a Quit Application action listener. This method does nothing on other platforms.
* @param l the action listener
*/
public static void removeQuitApplicationListener(ActionListener l)
{
MRJ4EventProxy.getInstance().removeQuitApplicationListener(l);
}
/**
* Add an Open Document action listener. When the listener is called, the
* {@code ActionEvent} received can be cast to a
* {@code net.roydesign.event.ApplicationEvent} which allows to get
* a reference to the file associated with the event. This method does
* nothing on other platforms.
* @param l the action listener
* @see net.roydesign.event.ApplicationEvent
*/
public static void addOpenDocumentListener(ActionListener l)
{
addOpenDocumentListener(l, null);
}
/**
* Add an Open Document action listener that receives events from the given source.
* Your application shouldn't normally call this method. Use the single
* parameter variant of this method instead.
* @param l the action listener
* @param source the source to use when firing the event
*/
public static void addOpenDocumentListener(ActionListener l, Object source)
{
MRJ4EventProxy.getInstance().addOpenDocumentListener(l, source);
}
/**
* Remove an Open Document action listener. This method does nothing on other platforms.
* @param l the action listener
*/
public static void removeOpenDocumentListener(ActionListener l)
{
MRJ4EventProxy.getInstance().removeOpenDocumentListener(l);
}
/**
* Add a Print Document action listener. When the listener is called, the
* {@code ActionEvent} received can be cast to a
* {@code net.roydesign.event.ApplicationEvent} which allows to get
* a reference to the file associated with the event. This method does
* nothing on other platforms.
* @param l the action listener
* @see net.roydesign.event.ApplicationEvent
*/
public static void addPrintDocumentListener(ActionListener l)
{
addPrintDocumentListener(l, null);
}
/**
* Add a Print Document action listener that receives events from the given source.
* Your application shouldn't normally call this method. Use the single
* parameter variant of this method instead.
* @param l the action listener
* @param source the source to use when firing the event
*/
public static void addPrintDocumentListener(ActionListener l, Object source)
{
MRJ4EventProxy.getInstance().addPrintDocumentListener(l, source);
}
/**
* Remove a Print Document action listener. This method does nothing on
* other platforms.
* @param l the action listener
*/
public static void removePrintDocumentListener(ActionListener l)
{
MRJ4EventProxy.getInstance().removePrintDocumentListener(l);
}
/**
* Check whether Swing uses the screen menu bar or not. This will
* only return true on Mac OS when the Mac-specific look and feel is
* used and the appropriate system properties are set that activate
* the frameless menu bar. This method always returns false on
* other platforms.
* @return whether Swing uses the screen menu bar
*/
public static boolean isSwingUsingScreenMenuBar()
{
LookAndFeel laf = UIManager.getLookAndFeel();
String name = laf.getClass().getName();
String prop = System.getProperty("apple.laf.useScreenMenuBar");
if (prop == null)
prop = System.getProperty("com.apple.macos.useScreenMenuBar");
return prop != null && "true".equalsIgnoreCase(prop) &&
("apple.laf.AquaLookAndFeel".equals(name) ||
name.startsWith("ch.randelshofer.quaqua"));
}
/**
* Set the AWT frameless menu bar. This menu bar is shown when
* no frame is visible. This state is normal for a Mac application
* whereas Java doesn't have any built-in way to do this because
* its menu bars must be attached to frames. On platforms other
* than Mac OS and Mac OS X, this method sets the menu bar
* internally so that it will be properly returned by a subsequent
* call to getFramelessMenuBar().
* @param menuBar the AWT menu bar to use as frameless menu bar
*/
public static void setFramelessMenuBar(MenuBar menuBar)
{
// Create the frame if needed
if (invisibleFrame == null)
{
invisibleFrame = new JFrame();
invisibleFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
invisibleFrame.setUndecorated(true);
invisibleFrame.setLocation(10000, 10000);
invisibleFrame.setSize(0, 0);
}
// Make it visible so the menubar will show, unless we're on a
// non-Mac platform where we just want to keep it internally
if (!invisibleFrame.isVisible())
invisibleFrame.setVisible(true);
// Set the menu bar
invisibleFrame.setMenuBar(menuBar);
}
/**
* Get the AWT frameless menu bar. This method is functional on
* all platforms.
* @return the AWT frameless menu bar
*/
public static MenuBar getFramelessMenuBar()
{
if (invisibleFrame != null)
return invisibleFrame.getMenuBar();
return null;
}
/**
* Set the Swing frameless menu bar. This menu bar is shown when
* no frame is visible. This state is normal for a Mac application
* whereas Java doesn't have any built-in way to do this because
* its menu bars must be attached to frames. Note that this method
* won't have any visual effect if the application doesn't use the
* Mac-specific look and feel and if the appropriate system
* properties are not set that activate the screen menu bar. Also,
* on platforms other than Mac OS and Mac OS X, this method sets
* the menu bar internally so that it will be properly returned by
* a subsequent call to getFramelessMenuBar().
* @param menuBar the Swing menu bar to use as frameless menu bar
*/
public static void setFramelessJMenuBar(JMenuBar menuBar)
{
// Make sure the invisible frame is the right class
if (invisibleFrame != null &&
!(invisibleFrame instanceof JFrame))
{
invisibleFrame.dispose();
invisibleFrame = null;
}
boolean usingScreenMenuBar = isSwingUsingScreenMenuBar();
// Create the frame if needed
if (invisibleFrame == null)
{
invisibleFrame = new JFrame();
((JFrame)invisibleFrame).setDefaultCloseOperation(
JFrame.DO_NOTHING_ON_CLOSE);
// We use reflection here because the setUndecorated() method
// only exists in Java 1.4 and up
try
{
Method mthd = invisibleFrame.getClass().getMethod("setUndecorated",
Boolean.TYPE);
mthd.invoke(invisibleFrame, Boolean.TRUE);
}
catch (Exception ex)
{
// We'll do without it then
}
invisibleFrame.setLocation(10000, 10000);
invisibleFrame.setSize(0, 0);
}
// Make it visible so the menubar will show, unless we're on a
// non-Mac platform or we're on a Mac but menubars are in the frame,
// where we just want to keep it internally
if (usingScreenMenuBar)
{
if (!invisibleFrame.isVisible())
invisibleFrame.setVisible(true);
}
// Set the menu bar
((JFrame)invisibleFrame).setJMenuBar(menuBar);
}
/**
* Get the Swing frameless menu bar. This method is functional on
* all platforms.
* @return the Swing frameless menu bar
*/
public static JMenuBar getFramelessJMenuBar()
{
if (invisibleFrame instanceof JFrame)
return ((JFrame)invisibleFrame).getJMenuBar();
return null;
}
/**
* Convert a four character code string to an integer. This method will
* normalize the given string in one of two ways. If the length of the
* string is zero, it will return the integer value 0, which provides
* support for unsetting entirely the creator or type of a file. On the
* other hand, if the given string is longer than zero, then it will be
* normalized to a length of four characters before being converted to an
* integer. If the string is too long it will be truncated and if it's too
* short it will be padded with spaces.
* @param code the four character code string
* @return the four character code as an integer
*/
public static int fourCharCodeToInt(String code)
{
final int numCharacters = 4;
final byte[] bytes = new byte[numCharacters];
int len = code.length();
if (len > 0)
{
byte[] bs = code.getBytes();
System.arraycopy(bs, 0, bytes, 0, Math.min(numCharacters, bs.length));
}
int val = 0;
for (int i = 0; i < numCharacters; i++)
{
if (i > 0)
val <<= 8;
val |= bytes[i] & 0xFF;
}
return val;
}
/**
* Convert a four character code integer to a string. This method will
* normalize the returned string in one of two ways. If the given integer
* is zero, then an empty string will be returned, which provides support
* for files that do not have a creator or type. Otherwise, the given
* integer is unpacked into a string with a length of four.
* @param code the four character code integer
* @return the four character code as a string
*/
public static String intToFourCharCode(int code)
{
if (code == 0)
return "";
byte[] bytes = {(byte)(code >> 24), (byte)(code >> 16), (byte)(code >> 8),(byte) code};
return new String(bytes);
}
/**
* Utility method to read and parse the content of the given Mac OS
* PkgInfo file, trying to extract the value of the given key. The
* method only supports two keys, "type" and "creator", since that's
* the only info that can be found in the PkgInfo file. If the given
* key is not found, the value {@code null} is returned.
* @param file the PkgInfo file
* @param key the key to look for
* @return the value of the key, or null
* @exception IOException when an I/O error occurs
*/
public static String parsePkgInfo(File file, String key) throws IOException
{
// This is nothing fancy but it does the job for now
/** @todo Should we set the encoding explicitly instead of using the platform default? */
String val = null;
LineNumberReader r = new LineNumberReader(new FileReader(file));
String line = r.readLine();
if (line != null)
{
if ("type".equals(key))
{
if (line.length() >= 4)
val = line.substring(0, 4);
}
else if ("creator".equals(key))
{
if (line.length() >= 8)
val = line.substring(4, 8);
}
}
r.close();
return val;
}
/**
* Utility method to read and parse the content of the given Mac OS
* Info.plist file, trying to extract the value of the given key. The
* keys that can be found in the Info.plist are defined by Apple. If
* the given key is not found, the value {@code null} is returned.
* @param file the Info.plist file
* @param key the key to look for
* @return the value of the key, or null
* @exception IOException when an I/O error occurs
*/
public static String parseInfoPlist(File file, CharSequence key) throws IOException
{
// This is nothing fancy but it does the job for now
/** @todo Should we set the encoding explicitly instead of using the platform default? */
String val = null;
LineNumberReader r = new LineNumberReader(new FileReader(file));
String line;
while ((line = r.readLine()) != null)
{
if (line.contains(key))
{
if ((line = r.readLine()) != null)
{
line = line.trim();
val = line.substring(line.indexOf('>') + 1, line.lastIndexOf('<'));
}
break;
}
}
r.close();
return val;
}
/**
* Get the name of the startup disk. This method will return the name
* of the startup disk, whether you are running on classic Mac OS or
* Mac OS X. On all other platforms, it throws an {@code IOException}.
* @return the name of the startup disk
* @exception IOException if an I/O error occurs
*/
public static String getStartupDisk() throws IOException
{
if (startupDisk == null)
{
/** @todo Can we do this with Cocoa instead (not NSAppleScript)? */
startupDisk = runAppleScript("tell application \"Finder\" to get name of startup disk");
}
return startupDisk;
}
/**
* Get the path to the application on disk. This method returns the full
* path of the application. Currently it only works with Mac OS X. If we
* figure out a way to make it work on classic Mac OS, we could make this
* method public. On all other platforms, it throws an {@code IOException}.
* @return the path to the application on disk
* @exception IOException if an I/O error occurs
*/
public static String getApplicationPath() throws IOException
{
if (applicationPath == null)
{
try
{
Class nsBundleClass =
Class.forName("com.apple.cocoa.foundation.NSBundle", true, cocoaClassLoader);
Method mainBundleMethod = nsBundleClass.getMethod("mainBundle", null);
Object bndl = mainBundleMethod.invoke(null, null);
Method bundlePathMethod = nsBundleClass.getMethod("bundlePath", null);
applicationPath = (String)bundlePathMethod.invoke(bndl, null);
}
catch (Exception ex)
{
throw new IOException(ex.getMessage(), ex);
}
}
return applicationPath;
}
/**
* Execute the given AppleScript script. This methods compiles and runs
* the script using the osascript tool in a shell. Because of this, it
* works only on Mac OS X.
* @param script the AppleScript script to execute
* @return the result
* @exception IOException if an I/O error occurs
*/
private static String runAppleScript(String script) throws IOException
{
Process p = Runtime.getRuntime().exec(new String[] {"osascript", "-e", script});
try (BufferedReader buffer = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
return buffer.lines().collect(Collectors.joining("\n"));
}
}
}