All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.roydesign.mac.MRJAdapter Maven / Gradle / Ivy

Go to download

MRJ Adapter is a wrapper around built in Java Virtual Machine APIs provided by Apple.

The newest version!
/*******************************************************************************

	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")); } } }