/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.ui.macosx; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.process.AbstractProcess; import com.mucommander.process.ProcessListener; import com.mucommander.process.ProcessRunner; /** * This class allows to run AppleScript code under Mac OS X, relying on the <code>osacript</code> command available * that comes with any install of Mac OS X. This command is used instead of the Cocoa-Java library which has been * deprecated by Apple.<br/> * Calls to {@link #execute(String, StringBuilder)} on any OS other than Mac OS X will always fail. * * <p> * <b>Important notes about character encoding</b>: * <ul> * <li>AppleScript 1.10- (Mac OS X 10.4 or lower) expects <i>MacRoman</i> encoding, not <i>UTF-8</i>. <b>That * means the script should only contain characters that are part of the MacRoman charset</b>; any character * that cannot be expressed in MacRoman will not be propertly interpreted.<br/> * The only way to pass Unicode text to a script is by reading it from a file. * See <a href="http://www.satimage.fr/software/en/unicode_and_applescript.html">http://www.satimage.fr/software/en/unicode_and_applescript.html</a> * for more information on how to do so. * </li> * <li>AppleScript 2.0+ (Mac OS X 10.5 and up) is fully Unicode-aware and will properly interpret any Unicode * character: "AppleScript is now entirely Unicode-based. Comments and text constants in scripts may contain * any Unicode characters, and all text processing is done in Unicode".<br/> * See <a href="http://www.apple.com/applescript/features/unicode.html">http://www.apple.com/applescript/features/unicode.html</a> * for more information. * </li> * </ul> * </p> * * @author Maxence Bernard */ public class AppleScript { private static final Logger LOGGER = LoggerFactory.getLogger(AppleScript.class); /** The UTF-8 encoding */ public final static String UTF8 = "UTF-8"; /** The MacRoman encoding */ public final static String MACROMAN = "MacRoman"; /** * Executes the given AppleScript and returns <code>true</code> if it completed its execution normally, i.e. without * any error. * The script's output is accumulated in the given <code>StringBuilder</code>. If the script completed its execution * normally, the buffer will contain the script's standard output. If the script failed because of an error in it, * the buffer will contain details about the error. * * <p>If the caller is not interested in the script's output, a <code>null</code> value can be passed which will * speed the execution up a little.</p> * * @param appleScript the AppleScript to execute * @param outputBuffer the StringBuilder that will hold the script's output, <code>null</code> for no output * @return true if the script was successfully executed, false if the */ public static boolean execute(String appleScript, StringBuilder outputBuffer) { // No point in going any further if the current OS is not Mac OS X if(!OsFamily.MAC_OS_X.isCurrent()) return false; LOGGER.debug("Executing AppleScript: "+appleScript); // Use the 'osascript' command to execute the AppleScript. The '-s o' flag tells osascript to print errors to // stdout rather than stderr. The AppleScript is piped to the process instead of passing it as an argument // ('-e' flag), for better control over the encoding and to remove any limitations on the maximum script size. String tokens[] = new String[] { "osascript", "-s", "o", }; OutputStreamWriter pout = null; try { // Execute the osascript command. AbstractProcess process = ProcessRunner.execute(tokens, outputBuffer==null?null:new ScriptOutputListener(outputBuffer, AppleScript.getScriptEncoding())); // Pipe the script to the osascript process. pout = new OutputStreamWriter(process.getOutputStream(), getScriptEncoding()); pout.write(appleScript); pout.close(); // Wait for the process to die int returnCode = process.waitFor(); LOGGER.debug("osascript returned code="+returnCode+", output="+ outputBuffer); if(returnCode!=0) { LOGGER.debug("osascript terminated abnormally"); return false; } return true; } catch(Exception e) { // IOException, InterruptedException // Shouldn't normally happen LOGGER.debug("Unexcepted exception while executing AppleScript", e); try { if(pout!=null) pout.close(); } catch(IOException e1) { // Can't do much about it } return false; } } /** * Returns the encoding that AppleScript uses on the current runtime environment: * <ul> * <li>{@link #UTF8} for AppleScript 2.0+ (Mac OS X 10.5 and up)</li> * <li>{@link #MACROMAN} for AppleScript 1.10- (Mac OS X 10.4 or lower)</li> * </ul> * * If {@link #MACROMAN} is used, the scripts passed to {@link #execute(String, StringBuilder)} should not contain * characters that are not part of the <i>MacRoman</i> charset or they will not be properly interpreted. * * @return the encoding that AppleScript uses on the current runtime environment */ public static String getScriptEncoding() { // - AppleScript 2.0+ (Mac OS X 10.5 and up) is fully Unicode-aware and expects a script in UTF-8 encoding. // - AppleScript 1.3- (Mac OS X 10.4 or lower) expects MacRoman encoding, not UTF-8. String encoding; if(OsVersion.MAC_OS_X_10_5.isCurrentOrHigher()) encoding = UTF8; else encoding = MACROMAN; return encoding; } /** * This ProcessListener accumulates the output of the 'osascript' command and suppresses the trailing '\n' character * from the script's output. */ private static class ScriptOutputListener implements ProcessListener { private StringBuilder outputBuffer; private String outputEncoding; private ScriptOutputListener(StringBuilder outputBuffer, String outputEncoding) { this.outputBuffer = outputBuffer; this.outputEncoding = outputEncoding; } //////////////////////////////////// // ProcessListener implementation // //////////////////////////////////// public void processOutput(byte[] buffer, int offset, int length) { try { outputBuffer.append(new String(buffer, offset, length, outputEncoding)); } catch(UnsupportedEncodingException e) { // The encoding is necessarily supported } } public void processOutput(String s) { } public void processDied(int returnValue) { // Remove the trailing "\n" character that osascript returns. int len = outputBuffer.length(); if(len>0 && outputBuffer.charAt(len-1)=='\n') outputBuffer.setLength(len-1); } } // The following commented method executes an AppleScript using the deprecated Cocoa-Java library. // We're now using the 'osascript' command instead, but this method is kept for the record in case Apple one day // decides to un-deprecate the Cocoa-Java library. // /** // * Executes the given AppleScript and returns the script's output if it was successfully executed, <code>null</code> // * if the script couldn't be compiled or if an error occurred while executing it. // * An empty string <code>""</code> is returned if the script doesn't output anything. // * // * @param appleScript the AppleScript to compile and execute // * @return the script's output, null if an error occurred while compiling or executing the script // */ // private static String executeAppleScript(String appleScript) { // AppLogger.finer("Executing AppleScript "+appleScript); // // int pool = -1; // // try { // // Quote from Apple Cocoa-Java doc: // // An autorelease pool is used to manage Foundation’s autorelease mechanism for Objective-C objects. // // NSAutoreleasePool provides Java applications access to autorelease pools. Typically it is not // // necessary for Java applications to use NSAutoreleasePools since Java manages garbage collection. // // However, some situations require an autorelease pool; for instance, if you start off a thread that // // calls Cocoa, there won’t be a top-level pool. // pool = NSAutoreleasePool.push(); // // NSMutableDictionary errorInfo = new NSMutableDictionary(); // NSAppleEventDescriptor eventDescriptor = new NSAppleScript(appleScript).execute(errorInfo); // if(eventDescriptor==null) { // AppLogger.fine("Caught AppleScript error: "+errorInfo.objectForKey(NSAppleScript.AppleScriptErrorMessage)); // // return null; // } // // String output = eventDescriptor.stringValue(); // Returns null if the script didn't output anything // AppLogger.finer("AppleScript output="+output); // // return output==null?"":output; // } // catch(Error e) { // // Can happen if Cocoa-java is not in the classpath // AppLogger.fine("Unexcepted error while executing AppleScript (cocoa-java not available?)", e); // // return null; // } // catch(Exception e) { // // Try block is not supposed to throw any exception, but this is low-level stuff so just to be safe // AppLogger.fine("Unexcepted exception while executing AppleScript", e); // // return null; // } // finally { // if(pool!=-1) // NSAutoreleasePool.pop(pool); // } // } }