public class

JAVADirector

extends JunctionActor
/*
 * Copyright (C) 2010 Stanford University
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package edu.stanford.junction.director;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONObject;

import edu.stanford.junction.Junction;
import edu.stanford.junction.JunctionMaker;
import edu.stanford.junction.JunctionException;
import edu.stanford.junction.SwitchboardConfig;
import edu.stanford.junction.api.activity.ActivityScript;
import edu.stanford.junction.api.activity.JunctionActor;
import edu.stanford.junction.api.activity.JunctionService;
import edu.stanford.junction.api.messaging.MessageHeader;
import edu.stanford.junction.provider.xmpp.XMPPSwitchboardConfig;

/**
 * This is a Director written for the JAVA platform.
 * It is capable of launching .JAR actors, as well as web-based actors.
 * The director activity also allows a user to see what's running on this machine.
 * 
 * TODO:
 * 	support properties for the director: directorSessionID, platformHints, security/access control
 * 
 * @author Ben
 *
 */
public class JAVADirector extends JunctionActor {
	public static final String DIRECTOR_SESSION = "jxservice"; // null will auto-generate
	private static final SwitchboardConfig mSbConfig = new XMPPSwitchboardConfig("prpl.stanford.edu");
	private static final JunctionMaker mMaker = JunctionMaker.getInstance(mSbConfig);
	private List<Activity>mActivities;

	public JAVADirector() {
		super("director");
		mActivities = new ArrayList<Activity>();
	}

	@Override
	public void onMessageReceived(MessageHeader header, JSONObject message) {
		try {
			if (message.has("action")) {
				String action = message.getString("action");
				if ("list".equals(action)) {
					JSONArray procs = new JSONArray();
					for (int i = mActivities.size()-1; i>=0; i--) {
						Activity activity = mActivities.get(i);
						try {
							activity.process.exitValue();
							// If we get this far, the process is terminated.
							System.out.println("exit value " + activity.process.exitValue());
							mActivities.remove(i);
						} catch (Exception e) {
							// No exit value means its still running.
							JSONObject obj = new JSONObject();
							obj.put("activity", activity.uri.toString());
							procs.put(obj);
						}
					}
					JSONObject msg = new JSONObject();
					msg.put("activities",procs);
					header.getReplyTarget().sendMessage(msg);
				}

				else if ("info".equals(action)) {
					// return Junction version, platform(s), and "hints" (headless, bigscreen, etc)
					// also a nickname.

					// maybe other known directors? owner info?

					// HINTS:
					// headless ~ server
					// bigscreen ~ TV or monitor attached
					// mobile ~ phone
					// nouser ~ no direct user input (bigscreen / headless)
					// keyboard? mouse?
				}
				
				else if ("cast".equals(action)) {
					String activityString = message.getString("activity");
					URI activityURI = new URI(activityString);
					
					// TODO: clean this up.
					ActivityScript script = mMaker.getActivityScript(activityURI);
					int p = activityString.indexOf("role=");
					if (p < 0) {
						System.out.println("Invitation does not specify a role.");
						return;
					}
					
					String role = activityString.substring(p+5);
					if (role.contains("&")) {
						int q = role.indexOf("&");
						role = role.substring(0,q);
					}
					
					JSONObject spec = script.getRoleSpec(role);
					JSONObject platforms = spec.getJSONObject("platforms");
					if (platforms.has("java")) {
						JSONObject javaplat = platforms.getJSONObject("java");
						if (javaplat.has("jar")) {
							URL jarURL = new URL(javaplat.getString("jar"));
							Process proc = launchJAR(jarURL,activityURI);
							
							if (proc != null) {
								mActivities.add(new Activity(activityURI,proc));
								
								InputStream is = proc.getInputStream();
								BufferedReader br = new BufferedReader( new InputStreamReader(is));
								String line;

								while ((line = br.readLine()) != null) {
							    	System.out.println(line);
								}
							}
						} else {
							System.out.println("Warning: JAVA platform specified but no JAR found.");
						}
					} 
					
					else if (platforms.has("web")) {
						// TODO: make sure this director isn't 'headless'
						// (add these properties)
						JSONObject webplat = platforms.getJSONObject("web");
						String webURL = webplat.getString("url");
						
						if (webURL.contains("?")) {
							webURL = webURL + "&";
						} else {
							webURL = webURL + "?";
						}
						webURL += "jxinvite="+URLEncoder.encode(activityString,"UTF-8");
						Process proc = BrowserControl.openUrl(webURL);
						if (proc != null) {
							mActivities.add(new Activity(activityURI,proc));
						}
					}
					
					else if (message.has("serviceName")) {
						String className = message.getString("serviceName");
						launchService(activityURI, script, className);
					} else {
						System.out.println("No action taken for " + message);
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private Process launchJAR(URL jarURL, URI activityURI) {
		final String JAR_PATH = "jars/";

		String jarName = JAR_PATH + "/" + jarURL.getPath().substring(1).replace("/", "-");
		File jarFile = new File(jarName);
		File tmpFile = new File(jarName+".tmp");
		if (!jarFile.exists() && !tmpFile.exists()) {
			try {
				FileOutputStream out = new FileOutputStream(tmpFile);

				InputStream in = jarURL.openStream();
				byte[] buf = new byte[4 * 1024];
				int bytesRead;
				while ((bytesRead = in.read(buf)) != -1) {
					out.write(buf, 0, bytesRead);
				}
				in.close();
				out.close();

				boolean res = tmpFile.renameTo(jarFile);
				if (!res) {
					throw new Exception("Could not rename file.");
				}
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		}

		if (!jarFile.exists()) {
			System.out.println("Failed to get JAR file " + jarFile.getName());
			return null;
		}

		// Launch the new JVM
		try {
			List<String>command = new ArrayList<String>();
			command.add("java");
			command.add("-jar");
			command.add(jarFile.getAbsolutePath());
			command.add(activityURI.toString());

			System.out.println("Executing: " + command.toString());
			
			ProcessBuilder pb = new ProcessBuilder(command);
			pb.directory(jarFile.getParentFile());
			pb.redirectErrorStream(true);
			Process p = pb.start();
			// TODO: make sure it worked

			return p;
		} catch (Exception e) {
			System.out.println("failed to launch JAR.");
			e.printStackTrace();
		}
		return null;
	}

	private void launchService(URI activityURI, ActivityScript script, String className) {	
		Class c = null;
		try {
			c = Class.forName(className);
		} catch (Exception e) {
			System.out.println("Could not find class for service " + className + ".");
		}


		JunctionService service = null;
		Method method = null;
		try {
			method = c.getMethod("newInstance");
			service = (JunctionService)method.invoke(null);
		} catch (Exception e) {
			System.out.println("No newInstance method found for " + c + ".");
		}


		String queryPart = activityURI.getQuery(); 
		System.out.println("query part is " + queryPart);
		String localRole = "Unknown";
		int i;
		if ((i = queryPart.indexOf("role=")) >= 0) {
			localRole = queryPart.substring(i+14);
			if ((i = localRole.indexOf("&"))>0) {
				localRole = localRole.substring(0,i);
			}
		}

		System.out.println("Setting service role to " + localRole);
		service.setRole(localRole);

		System.out.println("service actorID is " + service.getActorID());
		try {
			mMaker.newJunction(activityURI, script, service);
		} catch (JunctionException e) {
			e.printStackTrace();
		}
	}


	public static void main(String[] argv) {
		ActivityScript script = new ActivityScript();
		script.setActivityID(JunctionMaker.DIRECTOR_ACTIVITY);
		//script.addRolePlatform("director", "java", null);
		//script.addRolePlatform("director","web", null); 
		//
		script.setFriendlyName("Activity Director");
		
		// TODO: These should be in a "carrier" field
		// ( carrier; implementation; provider; ... )
		script.setSessionID(DIRECTOR_SESSION);

		JunctionActor director = new JAVADirector();
		try{
			Junction jx = mMaker.newJunction(URI.create("junction://prpl.stanford.edu/jxservice"), director);
			System.out.println("Launched director on " + jx.getInvitationURI());
			synchronized(director){
				try {
					director.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		catch(JunctionException e){
			e.printStackTrace(System.err);
		}
	}	
}

class Activity {
	public URI uri;
	public Process process;

	public Activity(URI uri,Process process) {
		this.uri = uri;
		this.process = process;
	}
}

/**
 * 
 * http://javaxden.blogspot.com/2007/09/launch-web-browser-through-java.html
 *
 */
class BrowserControl{ 
	/**
	 * Method to Open the Browser with Given URL
	 * @param url
	 */
	public static Process openUrl(String url){
		String os = System.getProperty("os.name");
		Runtime runtime=Runtime.getRuntime();
		try{
			// Block for Windows Platform
			if (os.startsWith("Windows")){
				String cmd = "rundll32 url.dll,FileProtocolHandler "+ url;
				Process p = runtime.exec(cmd);
				return p;
			}
			//Block for Mac OS
			else if(os.startsWith("Mac OS")){
				Class fileMgr = Class.forName("com.apple.eio.FileManager");
				Method openURL = fileMgr.getDeclaredMethod("openURL", new Class[] {String.class});
				openURL.invoke(null, new Object[] {url});
				return null; // TODO find a better way
			}
			//Block for UNIX Platform 
			else {
				String[] browsers = {"firefox", "chrome", "opera", "konqueror", "epiphany", "mozilla", "netscape" };
				String browser = null;
				for (int count = 0; count < browsers.length && browser == null; count++)
					if (runtime.exec(new String[] {"which", browsers[count]}).waitFor() == 0)
						browser = browsers[count];
				if (browser == null)
					throw new Exception("Could not find web browser");
				else 
					return runtime.exec(new String[] {browser, url});
			}
		}catch(Exception x){
			System.err.println("Exception occurd while invoking Browser!");
			x.printStackTrace();
			return null;
		}
	}
}