package org.opensourcephysics.media.core; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.BufferedReader; import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; import java.util.ArrayList; import java.util.HashSet; import java.util.Map; import java.util.TreeMap; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import javax.swing.Timer; import javax.swing.event.SwingPropertyChangeSupport; import org.opensourcephysics.controls.OSPLog; import org.opensourcephysics.controls.XMLControl; import org.opensourcephysics.controls.XMLControlElement; import org.opensourcephysics.display.Data; import org.opensourcephysics.display.OSPRuntime; import org.opensourcephysics.tools.Job; import org.opensourcephysics.tools.LocalJob; import org.opensourcephysics.tools.Tool; /** * A class to support the RMI transfer of data to the Tracker DataTrackTool. * * @author Douglas Brown */ public class DataTrackSupport { public static final String HOST = "localhost"; //$NON-NLS-1$ public static final int PORT = 1099; private static Tool remoteTool; private static Tool supportTool; private static XMLControl messageControl; private static Timer timer; private static PropertyChangeSupport support = new SwingPropertyChangeSupport(new Object()); private static HashSet<String> dataNames = new HashSet<String>(); private static boolean connected; // private constructor to prevent instantiation private DataTrackSupport() {} /** * Connects to Tracker and adds a PropertyChangeListener to listen for * Tracker messages and requests. This returns false if Tracker is * unavailable. The PropertyChangeListener is notified with a "tracker_ready" * PropertyChangeEvent when Tracker is ready to receive data. * * @param id a number to identify the data source (typically hashcode()) * @param listener the PropertyChangeListener to notify when the remote tool is ready * @return true if Tracker is available */ public static boolean connect(final int id, PropertyChangeListener listener) { if (!isTrackerAvailable()) return false; // don't add the same listener more than once support.removePropertyChangeListener(listener); support.addPropertyChangeListener(listener); // see if Tracker is running (remote tool available) if (getRemoteTool()!=null) { // if already connected, return immediately if (connected) { return true; } // otherwise send handshake message and return return sendHandshake(id); } // if not running, launch Tracker in separate VM // get the path to the tracker.jar file String trackerHome = (String)OSPRuntime.getPreference("TRACKER_HOME"); //$NON-NLS-1$ String trackerPath = new File(trackerHome, "tracker.jar").getAbsolutePath(); //$NON-NLS-1$ // assemble the command final ArrayList<String> cmd = new ArrayList<String>(); cmd.add("java"); //$NON-NLS-1$ cmd.add("-classpath"); //$NON-NLS-1$ cmd.add(trackerPath); cmd.add("org.opensourcephysics.cabrillo.tracker.deploy.TrackerStarter"); //$NON-NLS-1$ // create ProcessBuilder to execute the command final ProcessBuilder builder = new ProcessBuilder(cmd); // log the command String launchMessage = "launching Tracker with command "; //$NON-NLS-1$ for (String next: cmd) { launchMessage += next + " "; //$NON-NLS-1$ } OSPLog.config(launchMessage); // start the process startProcess(builder); // start timer to look for remoteTool and send handshake message if (timer==null) { timer = new Timer(500, new ActionListener() { public void actionPerformed(ActionEvent e) { if (getRemoteTool()!=null) { timer.stop(); sendHandshake(id); } } }); } timer.setInitialDelay(1000); timer.setRepeats(true); timer.start(); return true; } /** * Sends data to the remote DataTrackTool. Data must include (x, y) positions. * * @param id a number to identify the data source (typically hashcode()) * @param data the Data object to send * @return true if the data was sent successfully */ public static boolean sendData(int id, Data data) { if (data==null) return false; Map<String, Object> message = new TreeMap<String, Object>(); message.put("data", data); //$NON-NLS-1$ return sendMessage(id, message); } /** * Sends previously sent Data with newly appended values. * Note: DataTrackTool reads only the appended values. If previously * sent values have changed, use the sendData method instead. * * Note: Data should contain previously sent data plus appended data, * not appended data only. * * @param id a number to identify the data source (typically hashcode()) * @param data the Data with appended values * @return true if the data was sent successfully */ public static boolean sendAppendedData(int id, Data data) { if (data==null) return false; if (!dataNames.contains(data.getName())) { return sendData(id, data); } Map<String, Object> message = new TreeMap<String, Object>(); message.put("data", data); //$NON-NLS-1$ message.put("append", true); //$NON-NLS-1$ return sendMessage(id, message); } /** * Sends a message to the remote DataTrackTool in the form of a String-to-Object mapping. * The message may include Data as a "data"-to-Data mapping. * * @param id a number to identify the data source (typically hashcode()) * @param message the message to send * @return true if sent successfully */ public static boolean sendMessage(int id, Map<String, Object> message) { Tool tool = getRemoteTool(); if (tool==null) return false; // get message control and set properties based on info map XMLControl control = getMessageControl(id); Data data = null; for (String key: message.keySet()) { Object value = message.get(key); if (key.equals("data")) { //$NON-NLS-1$ data = (Data)value; } control.setValue(key, value); } try { tool.send(new LocalJob(control.toXML()), getSupportTool()); } catch (RemoteException e) { return false; } if (data!=null) { dataNames.add(data.getName()); } return true; } /** * Determines if a DataTrack-enabled Tracker is available on this machine. * This returns true if a tracker.jar file is found in the OSP preference * TRACKER_HOME. Requires Tracker version 5.00 (build-date 2015) or above. * * @return true if Tracker is available (whether or not it is running) */ public static boolean isTrackerAvailable() { String trackerHome = (String)OSPRuntime.getPreference("TRACKER_HOME"); //$NON-NLS-1$ if (trackerHome==null) { return false; } File file = new File(trackerHome, "tracker.jar"); //$NON-NLS-1$ if (!file.exists()) { return false; } try { JarFile jar = new JarFile(file); Manifest mf = jar.getManifest(); jar.close(); Attributes attributes = mf.getMainAttributes(); for (Object obj : attributes.keySet()) { String key = obj.toString(); if (key.contains("Build-Date")) { //$NON-NLS-1$ String val = attributes.getValue(key); int year = Integer.parseInt(val.substring(val.length()-4, val.length())); if (year<2015) return false; } } } catch (Exception e) { return false; } return true; } /** * Determines if data was requested by Tracker when the current process was started. * EJS models should check this when first launched and send data if true. * * @return true if data was requested */ public static boolean isDataRequested() { return System.getenv("DATA_REQUESTED")!=null; //$NON-NLS-1$ } //____________________________ private methods _______________________________ /** * Sends a handshake message. * * @param id a number to identify the data source (typically hashcode()) */ private static boolean sendHandshake(int id) { XMLControl control = getMessageControl(id); control.setValue("handshake", true); //$NON-NLS-1$ control.setValue("jar_path", OSPRuntime.getLaunchJarPath()); //$NON-NLS-1$ try { remoteTool.send(new LocalJob(control.toXML()), getSupportTool()); } catch (RemoteException e) { return false; } return true; } /** * Starts a ProcessBuilder and handles its output and error streams. * * @param builder the ProcessBuilder */ private static void startProcess(final ProcessBuilder builder) { // start the process and wait for it to finish Runnable runner = new Runnable() { public void run() { try { Process process = builder.start(); // read output stream from the process--important so process doesn't block InputStream is = process.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); String line; while ((line = br.readLine()) != null) { System.out.println(line); } br.close(); int result = process.waitFor(); // if process returns with exit code > 0, print it's error stream if (result > 0) { isr = new InputStreamReader(process.getErrorStream()); br = new BufferedReader(isr); while ((line = br.readLine()) != null) { System.err.println(line); } br.close(); } } catch (Exception ex) { ex.printStackTrace(); } } }; new Thread(runner).start(); } /** * Gets a remote copy of the tracker.DataTrackTool. * * @return the remote tool, or null if not available */ private static Tool getRemoteTool() { if (remoteTool==null) { try { Registry registry = LocateRegistry.getRegistry(HOST, PORT); remoteTool = (Tool)registry.lookup("DataTrackTool"); //$NON-NLS-1$ return remoteTool; } catch (Exception ex) { } } return remoteTool; } /** * Gets the support tool. * * @return the support tool */ private static Tool getSupportTool() { if (supportTool==null) { try { supportTool = new SupportTool(); } catch (RemoteException e) { } } return supportTool; } //____________________________ static methods _______________________________ /** * Clears and returns the message XMLControl. * * @return the XMLCntrol */ public static XMLControl getMessageControl(int id) { if (messageControl==null) { messageControl = new XMLControlElement(new Message()); } for (String name: messageControl.getPropertyNames()) { messageControl.setValue(name, null); } messageControl.setValue("sourceID", id); //$NON-NLS-1$ return messageControl; } //____________________________ inner classes _______________________________ /** * A remote Tool sent to Tracker so it can communicate with data clients. */ private static class SupportTool extends UnicastRemoteObject implements Tool { /** * Constructor required to throw RemoteException. */ protected SupportTool() throws RemoteException { super(); } @Override public void send(Job job, Tool replyTo) throws RemoteException { XMLControl control = new XMLControlElement(); control.readXML(job.getXML()); if (control.failedToRead()) return; int sourceID = control.getInt("sourceID"); //$NON-NLS-1$ if (control.getBoolean("handshake")) { //$NON-NLS-1$ connected = true; support.firePropertyChange("tracker_ready", sourceID, null); //$NON-NLS-1$ } else if (control.getBoolean("exiting")) { //$NON-NLS-1$ remoteTool = null; connected = false; support.firePropertyChange("tracker_exited", null, null); //$NON-NLS-1$ } } } /** * A class used to send messages. */ private static class Message {} }