View Javadoc

1   /*$Id$
2    * Created on 26-Jul-2005
3    *
4    * Copyright (C) AstroGrid. All rights reserved.
5    *
6    * This software is published under the terms of the AstroGrid 
7    * Software License version 1.2, a copy of which has been included 
8    * with this distribution in the LICENSE.txt file.  
9    *
10  **/
11  package org.astrogrid.acr;
12  
13  import net.ladypleaser.rmilite.Client;
14  
15  import org.astrogrid.acr.builtin.ACR;
16  import org.astrogrid.acr.builtin.Shutdown;
17  import org.astrogrid.acr.builtin.ShutdownListener;
18  import org.astrogrid.acr.system.ApiHelp;
19  
20  import org.apache.commons.logging.Log;
21  import org.apache.commons.logging.LogFactory;
22  
23  
24  import java.awt.HeadlessException;
25  import java.io.BufferedReader;
26  import java.io.File;
27  import java.io.FileNotFoundException;
28  import java.io.FileReader;
29  import java.io.IOException;
30  import java.lang.reflect.InvocationTargetException;
31  import java.lang.reflect.Method;
32  import java.net.MalformedURLException;
33  import java.net.URL;
34  import java.rmi.NotBoundException;
35  import java.rmi.RemoteException;
36  
37  import javax.swing.JOptionPane;
38  
39  /*** Find or create an ACR server, and return an interface to that service.
40   * 
41        * first attempts to connect to a running instance using RMI, on a port defined in the file <tt>~/.acr-rmi-port</tt> (which is written by a running ACR instance<br />
42       * failing that, tries to create an external instance (will only work if running under java web start), and then connect to that using RMI<br />
43       * failing that, trys to create an instance internally (will only work if implementation classes are on classpath),<br />     
44       * the interface returned will either be a  rmi stub or direct instance, depending on how the ACR was found.
45       * <br />
46       * No matter how the acr is found, the ACR returned is a singleton - it is stored in this class for simple access the next time
47   * @author Noel Winstanley nw@jb.man.ac.uk 26-Jul-2005
48   * @example
49   * <pre>
50   * import org.astrogrid.acr.builtin.ACR;
51   * import org.astrogrid.acr.Finder;
52   * Finder f = new Finder();
53   * ACR acr = f.find(); 
54   * </pre>
55   *@see org.astrogrid.acr.builtin.ACR
56   */
57  public class Finder {
58      /*** Webstart URL for the ACR */
59      public static final String ACR_JNLP_URL = "http://software.astrogrid.org/jnlp/beta/workbench/workbench.jnlp";
60      /***
61       * Commons Logger for this class
62       */
63      private static final Log logger = LogFactory.getLog(Finder.class);
64  
65      /*** Construct a new Finder
66       * 
67       */
68      public Finder() {
69          super();
70      }
71      
72      /*** Find or create a running ACR server.
73       *      
74       * first attempts to connect to a running instance, on a port defined in the file <tt>~/.acr-rmi-port</tt> (which is written by a running ACR instance<br />
75       * failing that, tries to create an external instance (will only work if running under java web start) and then connect to that using RMI<br/>
76       * failing that, trys to create an instance internally (will only work if implementation classes are on classpath),<br/>     
77       * @return an interface to the running ACR - depending on how connected will either be a direct instance or a remote stub - although this makes no difference to the consumer.
78       * The instance returned is a singleton - i.e. all subsequent calls to {@link #find} will return the same object.
79       * @throws ACRException if all options fail
80       * 
81       * */
82      public synchronized ACR find()  throws ACRException{
83      	boolean tryToStartIfNotRunning = true;
84      	boolean warnUserBeforeStarting = false;
85           
86          return find(tryToStartIfNotRunning, warnUserBeforeStarting);
87  
88      }
89  
90      /***
91       * Find or create a running ACR server.
92       * @see #find()
93       * @param tryToStartIfNotRunning if false, will not attempt to start an ACR, but merely return NULL if there isn't one running
94       * @param warnUserBeforeStarting if true, will warn the user before attempting start an ACR, giving him the chance to start one manually
95       * @return
96       * @throws ACRException
97       */
98  	public ACR find(boolean tryToStartIfNotRunning, boolean warnUserBeforeStarting) throws ACRException {
99  		if (acr == null) {
100             acr= createACR(tryToStartIfNotRunning, warnUserBeforeStarting);
101             try { // attempt to register a listener, it it'll let me: use it to remove singleton when host vanishes.
102                 Shutdown sd = (Shutdown)acr.getService(Shutdown.class);
103                 sd.addShutdownListener(new ShutdownListener() {
104                     public void halting() {
105                         logger.info("Host ACR shutting down");
106                         Finder.this.acr = null;
107                     }
108                     public String lastChance() {
109                         return null; // won't ever object.
110                     }
111                 });
112             } catch (ACRException e) {
113                 logger.warn("Failed to register shutdown listener - no matter",e);
114             }
115                 
116         }
117 		return acr;
118 	}
119     
120     /***
121      * @param userWarning If not null, prompt the user <b>before</b> attempting to start an external ACR.
122      * @param tryToStartIfNotRunning if false, don't start an external ACR if there isn't one. 
123      * @throws NoAvailableACRException
124      */
125     private ACR createACR(boolean tryToStartIfNotRunning, boolean warnUser) throws ACRException {
126     	logger.info("Searching for acr");
127     	ACR result = null;
128     	try {
129     		result = connectExternal();
130     		if (result != null) {
131     			return result;
132     		}
133     	} catch (Exception e) {
134     		logger.warn("Failed to connect to existing external acr",e);
135     	}                    
136     	
137     	try {
138     		result = createInternal();
139     		if (result != null) {
140     			return result;
141     		}    
142     	} catch (Exception e) {
143     		logger.warn("Failed to create internal acr",e);
144     	}
145     	// hmm, try starting external service
146     	if (tryToStartIfNotRunning) {
147     		try {
148     			if (warnUser) {
149     				try {
150     					//warn before attempting to start, it's only polite
151     					JOptionPane.showMessageDialog(null,"<html>This application requires the ACR.<br>Either start it yourself and press 'OK', or simply press 'OK' to auto-start it.</html>",
152     							"ACR Required",JOptionPane.INFORMATION_MESSAGE);
153     					//Try again, maybe they started it now
154     					result = connectExternal();
155     					if (result != null) {
156     						return result;
157     					}
158     				} catch (HeadlessException he) {
159     					logger.warn("Not running in a ui environment - can't ask permission, so starting ACR anyway");
160     				} catch (Exception e) {
161     					logger.warn("Failed to connect to external acr.",e);
162     				}  
163     			}
164     			logger.info("Still no ACR, so attempt to create one");
165     			createExternal();
166     			// need to wait some time to allow external to bootup (and maybe download).
167     			
168     			long now = System.currentTimeMillis();
169     			final int WAIT_TIME = (2 * 60  * 1000); // 2 minutes
170 				long tooLong = now + WAIT_TIME ; 
171     			while (connectExternal()==null) {
172     				if (System.currentTimeMillis()>tooLong) {
173     					//prompt and see if we want to carry on waiting
174     		    		int continueToWait;
175     		    		try {
176     		    			continueToWait = JOptionPane.showConfirmDialog(null,"<html>The ACR hasn't started yet.  Press OK to continue waiting, Cancel to abort.</html>","ACR not started",JOptionPane.OK_CANCEL_OPTION,JOptionPane.WARNING_MESSAGE);
177     		    		} catch (HeadlessException e) {
178     		    			logger.warn("Not running in a ui environment - can't prompt");
179     		    			continueToWait = JOptionPane.CANCEL_OPTION;
180     		    		}
181     		    		if (continueToWait == JOptionPane.CANCEL_OPTION) {
182     		    			break;
183     		    		} else {
184     		    			tooLong += WAIT_TIME;
185     		    		}
186     				}
187     				Thread.sleep(5000); // 5 seconds.
188     			}
189     			result = connectExternal();
190     			if (result != null) {
191     				return result;
192     			}
193     		} catch (Exception e) {
194     			logger.warn("Failed to create external acr",e);
195     		}            
196     		// finally - try prompting the user.
197     		int dialogueResult;
198     		try {
199     			dialogueResult = JOptionPane.showConfirmDialog(null,"<html><b>Please start the ACR by hand</b><br>When started press 'Ok'. To halt press 'Cancel'","Unable to automatically start ACR",JOptionPane.OK_CANCEL_OPTION,JOptionPane.WARNING_MESSAGE);
200     		} catch (HeadlessException e) {
201     			logger.warn("Not running in a ui environment - can't prompt");
202     			dialogueResult = JOptionPane.NO_OPTION;
203     		}
204     		
205     		if (dialogueResult == JOptionPane.YES_OPTION) { // try to connect again.
206     			try {
207     				result = connectExternal();
208     				if (result != null) {
209     					return result;
210     				}
211     			} catch (Exception e) {
212     				logger.warn("Failed to connect to external acr, after user claimed to start one.",e);
213     				// could loop here?
214     			}                  
215     		}
216     	}
217     	// fallen through everything.
218     	throw new ACRException("Failed to find or create an ACR to connect to");
219     }
220 
221     /*** connect to an external acr.
222      * @return
223      * @throws FileNotFoundException
224      * @throws NumberFormatException
225      * @throws IOException
226      * @throws RemoteException
227      * @throws NotBoundException
228      */
229     private ACR connectExternal() throws FileNotFoundException, NumberFormatException, IOException, RemoteException, NotBoundException {
230     	File conf = configurationFile();  
231     	if (!conf.exists()) {
232     		logger.info("No configuration file - suggests an acr instance is not running at the moment");
233     		return null;    		
234     	}
235     	
236     	logger.info("configuration file indicates an acr is already running");                
237     	BufferedReader br = new BufferedReader(new FileReader(conf));                
238     	int port = Integer.parseInt(br.readLine());
239     	br.close(); //Otherwise the file can be locked and left undeleted when the ACR shuts down.
240     	logger.info("Port determined to be " + port);
241     	final Client client = new Client("localhost",port);
242     	final ApiHelp api = (ApiHelp)client.lookup(ApiHelp.class);
243     	ACR newAcr = new ACR() {
244     		
245     		public Object getService(Class interfaceClass) throws ACRException, NotFoundException {
246     			if (interfaceClass.equals(ACR.class)) {
247     				return this;
248     			}
249     			try {
250     				registerListeners(interfaceClass);
251     				return client.lookup(interfaceClass);
252     			} catch (RemoteException e) {
253     				throw new ACRException(e);
254     			} catch (NotBoundException e) {
255     				throw new NotFoundException(e);
256     			}
257     		}
258     		
259     		private void registerListeners(Class c) {
260     			Method[] arr = c.getMethods();
261     			for (int i = 0; i < arr.length; i++) {
262     				Method m = arr[i];
263     				Class[] ps = m.getParameterTypes();
264     				for (int j = 0; j < ps.length; j++) {
265     					maybeRegister(ps[j]);
266     				}
267     				Class ret = m.getReturnType();
268     				maybeRegister(ret);
269     			}
270     		}
271     		private void maybeRegister(Class c) {
272     			if (c.isInterface() &&   c.getName().endsWith("Listener")) {
273     				logger.debug("Exporting interface " + c.getName());
274     				client.exportInterface(c);
275     			}
276     		}
277     		
278     		
279     		public Object getService(String componentName) throws ACRException, NotFoundException {
280     			String className = api.interfaceClassName(componentName);
281     			Class clazz = null;
282     			try {
283     				clazz = Class.forName(componentName); // makes the interface more user friendly.
284     			} catch (ClassNotFoundException e) {
285     				// try to resolve on the server
286     				try {
287     					clazz = Class.forName(className);
288     				} catch (ClassNotFoundException e1) {
289     					throw new NotFoundException(e1);
290     				}
291     			}
292     			return getService(clazz);
293     		}
294     	};
295     	//TODO check that the ACR is booted?  Sometimes the config file is present before the ACR is ready.
296     	
297     	return newAcr;           
298     	
299     }
300    
301     
302     private ACR acr;
303     
304     public static final File configurationFile() {
305         File homeDir = new File(System.getProperty("user.home"));
306         return new File(homeDir,".acr-rmi-port");
307     }
308     
309     
310     
311     /*** create an external instance of the acr 
312      * would like to be able to do this by fetching the jnlp file - however, don't know that this process is 
313      * running under javaws - and so need to find another library to control the system browser.
314      * blechh.
315      * for now, only create an external acr if running under javaws.
316      * @todo add browser control lib to do this in other circumstances.
317      * @todo think about using jnlp installer extensions to do this?
318      * @return
319      * @throws NoSuchMethodException
320      * @throws SecurityException
321      * @throws MalformedURLException
322      * @throws InvocationTargetException
323      * @throws IllegalAccessException
324      * @throws IllegalArgumentException
325      * @throws ClassNotFoundException
326      * @throws ClassNotFoundException
327      */
328     private void createExternal() throws SecurityException, NoSuchMethodException, MalformedURLException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, ClassNotFoundException {
329         Method showMethod = null;
330         Object methodTarget = null;        
331         try {
332              Class managerClass = Class.forName("javax.jnlp.ServiceManager");
333              Method lookupMethod= managerClass.getMethod("lookup",new Class[]{String.class});
334              methodTarget = lookupMethod.invoke(null,new Object[]{"javax.jnlp.BasicService"});
335              showMethod = methodTarget.getClass().getMethod("showDocument",new Class[]{URL.class});
336         } catch (ClassNotFoundException e) {
337             logger.info("Not running under java web start");
338         }
339         if (showMethod == null) { // try something else.
340             try {
341             Class jdicClass = Class.forName("org.jdesktop.jdic.desktop.Desktop");
342             showMethod= jdicClass.getMethod("browse",new Class[]{URL.class});    
343             } catch (ClassNotFoundException e1) {
344                 logger.info("Not running with jdic libs");
345             }
346         }
347         
348         if (showMethod == null) {
349             throw new ClassNotFoundException("Can't find any class that can control the system browser");
350         }
351         URL url = new URL(ACR_JNLP_URL);
352         showMethod.invoke(methodTarget,new Object[]{url});        
353     }
354     
355     private ACR createInternal() throws InstantiationException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
356         // all done without referencing the class by name - as may not be available on the classpath.
357         try {
358             Class buildClass = Class.forName("org.astrogrid.desktop.BuildInprocessACR");
359             Object o = buildClass.newInstance();
360             // start the acr.
361             Method m = buildClass.getMethod("start",null);
362             m.invoke(o,null);
363             // return the acr instance
364             m = buildClass.getMethod("getACR",null);
365             return (ACR)m.invoke(o,null);
366         } catch (ClassNotFoundException e) {            
367             logger.info("ACR implementation classes not available - must connect to a remote acr",e);
368         } 
369         return null;
370     }
371 
372 }
373 
374 
375 /* 
376 $Log$
377 Revision 1.1  2006/05/27 23:57:20  jdt
378 temporary addition
379 
380 Revision 1.8  2006/05/22 20:26:52  jdt
381 Updated the auto-start URL.  Will need to think how we now handle
382 the multiple AR flavours
383 
384 Revision 1.7  2006/04/14 19:41:46  jdt
385 Added some new features to the Finder and made it more robust.  It's now more of a rats' nest though and so could do with some refactoring.
386 
387 Revision 1.6  2006/02/02 14:19:47  nw
388 fixed up documentation.
389 
390 Revision 1.5  2005/08/25 16:59:44  nw
391 1.1-beta-3
392 
393 Revision 1.4  2005/08/16 13:16:23  nw
394 doc fix
395 
396 Revision 1.3  2005/08/12 12:42:05  nw
397 finished documentation effort.
398 
399 Revision 1.2  2005/08/12 08:45:15  nw
400 souped up the javadocs
401 
402 Revision 1.1  2005/08/11 10:15:00  nw
403 finished split
404 
405 Revision 1.1  2005/08/05 11:46:55  nw
406 reimplemented acr interfaces, added system tests.
407  
408 */