001    /*
002     * This file is part of muCommander, http://www.mucommander.com
003     * Copyright (C) 2002-2008 Maxence Bernard
004     *
005     * muCommander is free software; you can redistribute it and/or modify
006     * it under the terms of the GNU General Public License as published by
007     * the Free Software Foundation; either version 3 of the License, or
008     * (at your option) any later version.
009     *
010     * muCommander is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    
019    package com.mucommander.ui.macosx;
020    
021    import com.mucommander.Debug;
022    import com.mucommander.process.AbstractProcess;
023    import com.mucommander.process.ProcessListener;
024    import com.mucommander.process.ProcessRunner;
025    import com.mucommander.runtime.OsFamilies;
026    import com.mucommander.runtime.OsVersions;
027    
028    import java.io.IOException;
029    import java.io.OutputStreamWriter;
030    import java.io.UnsupportedEncodingException;
031    
032    /**
033     * This class allows to run AppleScript code under Mac OS X, relying on the <code>osacript</code> command available
034     * that comes with any install of Mac OS X. This command is used instead of the Cocoa-Java library which has been
035     * deprecated by Apple.<br/>
036     * Calls to {@link #execute(String, StringBuffer)} on any OS other than Mac OS X will always fail.
037     *
038     * <p>
039     * <b>Important notes about character encoding</b>:
040     * <ul>
041     *   <li>AppleScript 1.10- (Mac OS X 10.4 or lower) expects <i>MacRoman</i> encoding, not <i>UTF-8</i>. <b>That
042     *       means the script should only contain characters that are part of the MacRoman charset</b>; any character
043     *       that cannot be expressed in MacRoman will not be propertly interpreted.<br/>
044     *       The only way to pass Unicode text to a script is by reading it from a file.
045     *       See <a href="http://www.satimage.fr/software/en/unicode_and_applescript.html">http://www.satimage.fr/software/en/unicode_and_applescript.html</a>
046     *       for more information on how to do so.
047     *   </li>
048     *   <li>AppleScript 2.0+ (Mac OS X 10.5 and up) is fully Unicode-aware and will properly interpret any Unicode
049     *       character: "AppleScript is now entirely Unicode-based. Comments and text constants in scripts may contain
050     *       any Unicode characters, and all text processing is done in Unicode".<br/>
051     *       See <a href="http://www.apple.com/applescript/features/unicode.html">http://www.apple.com/applescript/features/unicode.html</a>
052     *       for more information.
053     *   </li>
054     * </ul>
055     * </p>
056     *
057     * @author Maxence Bernard
058     */
059    public class AppleScript {
060    
061        /** The UTF-8 encoding */
062        public final static String UTF8 = "UTF-8";
063    
064        /** The MacRoman encoding */
065        public final static String MACROMAN = "MacRoman";
066    
067    
068        /**
069         * Executes the given AppleScript and returns <code>true</code> if it completed its execution normally, i.e. without
070         * any error.
071         * The script's output is accumulated in the given <code>StringBuffer</code>. If the script completed its execution
072         * normally, the buffer will contain the script's standard output. If the script failed because of an error in it,
073         * the buffer will contain details about the error.
074         *
075         * <p>If the caller is not interested in the script's output, a <code>null</code> value can be passed which will
076         * speed the execution up a little.</p>
077         *
078         * @param appleScript the AppleScript to execute
079         * @param outputBuffer the StringBuffer that will hold the script's output, <code>null</code> for no output
080         * @return true if the script was succesfully executed, false if the
081         */
082        public static boolean execute(String appleScript, StringBuffer outputBuffer) {
083            // No point in going any futher if the current OS is not Mac OS X
084            if(!OsFamilies.MAC_OS_X.isCurrent())
085                return false;
086    
087            if(Debug.ON) Debug.trace("Executing AppleScript: "+appleScript);
088    
089            // Use the 'osascript' command to execute the AppleScript. The '-s o' flag tells osascript to print errors to
090            // stdout rather than stderr. The AppleScript is piped to the process instead of passing it as an argument
091            // ('-e' flag), for better control over the encoding and to remove any limitations on the maximum script size.
092            String tokens[] = new String[] {
093                "osascript",
094                "-s",
095                "o",
096            };
097    
098            OutputStreamWriter pout = null;
099            try {
100                // Execute the osascript command.
101                AbstractProcess process = ProcessRunner.execute(tokens, outputBuffer==null?null:new ScriptOutputListener(outputBuffer, AppleScript.getScriptEncoding()));
102    
103                // Pipe the script to the osascript process.
104                pout = new OutputStreamWriter(process.getOutputStream(), getScriptEncoding());
105                pout.write(appleScript);
106                pout.close();
107    
108                // Wait for the process to die
109                int returnCode = process.waitFor();
110    
111                if(Debug.ON) Debug.trace("osascript returned code="+returnCode+", output="+ outputBuffer);
112    
113                if(returnCode!=0) {
114                    if(Debug.ON) Debug.trace("osascript terminated abnormally");
115                    return false;
116                }
117    
118                return true;
119            }
120            catch(Exception e) {        // IOException, InterruptedException
121                // Shouldn't normally happen
122                if(Debug.ON) {
123                    Debug.trace("Unexcepted exception while executing AppleScript: "+e);
124                    e.printStackTrace();
125                }
126    
127                try {
128                    if(pout!=null)
129                        pout.close();
130                }
131                catch(IOException e1) {
132                    // Can't do much about it
133                }
134    
135                return false;
136            }
137        }
138    
139        /**
140         * Returns the encoding that AppleScript uses on the current runtime environment:
141         * <ul>
142         *   <li>{@link #UTF8} for AppleScript 2.0+ (Mac OS X 10.5 and up)</li>
143         *   <li>{@link #MACROMAN} for AppleScript 1.10- (Mac OS X 10.4 or lower)</li>
144         * </ul>
145         *
146         * If {@link #MACROMAN} is used, the scripts passed to {@link #execute(String, StringBuffer)} should not contain
147         * characters that are not part of the <i>MacRoman</i> charset or they will not be properly interpreted.
148         *
149         * @return the encoding that AppleScript uses on the current runtime environment
150         */
151        public static String getScriptEncoding() {
152            // - AppleScript 2.0+ (Mac OS X 10.5 and up) is fully Unicode-aware and expects a script in UTF-8 encoding.
153            // - AppleScript 1.3- (Mac OS X 10.4 or lower) expects MacRoman encoding, not UTF-8.
154            String encoding;
155            if(OsVersions.MAC_OS_X_10_5.isCurrentOrHigher())
156                encoding = UTF8;
157            else
158                encoding = MACROMAN;
159    
160            return encoding;
161        }
162    
163    
164        /**
165         * This ProcessListener accumulates the output of the 'osascript' command and suppresses the trailing '\n' character
166         * from the script's output.
167         */
168        private static class ScriptOutputListener implements ProcessListener {
169    
170            private StringBuffer outputBuffer;
171            private String outputEncoding;
172    
173            private ScriptOutputListener(StringBuffer outputBuffer, String outputEncoding) {
174                this.outputBuffer = outputBuffer;
175                this.outputEncoding = outputEncoding;
176            }
177    
178            ////////////////////////////////////
179            // ProcessListener implementation //
180            ////////////////////////////////////
181    
182            public void processOutput(byte[] buffer, int offset, int length) {
183                try {
184                    outputBuffer.append(new String(buffer, offset, length, outputEncoding));
185                }
186                catch(UnsupportedEncodingException e) {
187                    // The encoding is necessarily supported
188                }
189            }
190    
191            public void processOutput(String s) {
192            }
193    
194            public void processDied(int returnValue) {
195                // Remove the trailing "\n" character that osascript returns.
196                int len = outputBuffer.length();
197                if(len>0 && outputBuffer.charAt(len-1)=='\n')
198                    outputBuffer.setLength(len-1);    
199            }
200        }
201    
202    
203        // The following commented method executes an AppleScript using the deprecated Cocoa-Java library.
204        // We're now using the 'osascript' command instead, but this method is kept for the record in case Apple one day
205        // decides to un-deprecate the Cocoa-Java library.
206    
207    //    /**
208    //     * Executes the given AppleScript and returns the script's output if it was successfully executed, <code>null</code>
209    //     * if the script couldn't be compiled or if an error occurred while executing it.
210    //     * An empty string <code>""</code> is returned if the script doesn't output anything.
211    //     *
212    //     * @param appleScript the AppleScript to compile and execute
213    //     * @return the script's output, null if an error occurred while compiling or executing the script
214    //     */
215    //    private static String executeAppleScript(String appleScript) {
216    //        if(Debug.ON) Debug.trace("Executing AppleScript "+appleScript);
217    //
218    //        int pool = -1;
219    //
220    //        try {
221    //            // Quote from Apple Cocoa-Java doc:
222    //            // An autorelease pool is used to manage Foundation’s autorelease mechanism for Objective-C objects.
223    //            // NSAutoreleasePool provides Java applications access to autorelease pools. Typically it is not
224    //            // necessary for Java applications to use NSAutoreleasePools since Java manages garbage collection.
225    //            // However, some situations require an autorelease pool; for instance, if you start off a thread that
226    //            // calls Cocoa, there won’t be a top-level pool.
227    //            pool = NSAutoreleasePool.push();
228    //
229    //            NSMutableDictionary errorInfo = new NSMutableDictionary();
230    //            NSAppleEventDescriptor eventDescriptor = new NSAppleScript(appleScript).execute(errorInfo);
231    //            if(eventDescriptor==null) {
232    //                if(Debug.ON)
233    //                    Debug.trace("Caught AppleScript error: "+errorInfo.objectForKey(NSAppleScript.AppleScriptErrorMessage));
234    //
235    //                return null;
236    //            }
237    //
238    //            String output = eventDescriptor.stringValue();  // Returns null if the script didn't output anything
239    //            if(Debug.ON) Debug.trace("AppleScript output="+output);
240    //
241    //            return output==null?"":output;
242    //        }
243    //        catch(Error e) {
244    //            // Can happen if Cocoa-java is not in the classpath
245    //            if(Debug.ON) Debug.trace("Unexcepted error while executing AppleScript (cocoa-java not available?): "+e);
246    //
247    //            return null;
248    //        }
249    //        catch(Exception e) {
250    //            // Try block is not supposed to throw any exception, but this is low-level stuff so just to be safe
251    //            if(Debug.ON) Debug.trace("Unexcepted exception while executing AppleScript: "+e);
252    //
253    //            return null;
254    //        }
255    //        finally {
256    //            if(pool!=-1)
257    //                NSAutoreleasePool.pop(pool);
258    //        }
259    //    }
260    }