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 }