content: /**
   *  Copyright (c) 2013-2016 Angelo ZERR and Genuitec LLC.
   *  All rights reserved. This program and the accompanying materials
   *  are made available under the terms of the Eclipse Public License v1.0
   *  which accompanies this distribution, and is available at
   *  http://www.eclipse.org/legal/epl-v10.html
   *
   *  Contributors:
   *  Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation
   *  Piotr Tomiak <piotr@genuitec.com> - support for tern.js debugging
   */
  package tern.server.nodejs.process;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.List;
  
  import tern.TernException;
  import tern.server.nodejs.NodejsTernHelper;
  
  /**
   * node.js process which starts tern server with node.js
   */
  public abstract class AbstractNodejsProcess implements INodejsProcess {
  
  	/**
  	 * The node.js base dir.
  	 */
  	protected final File nodejsBaseDir;
  
  	/**
  	 * The project dir where .tern-project is hosted.
  	 */
  	protected final File projectDir;
  
  	private INodejsLaunchConfiguration launchConfiguration;
  
  	/**
  	 * Port of the node.js server.
  	 */
  	private Integer port;
  
  	/**
  	 * Elapsed time to start node.js process.
  	 */
  	private long elapsedSartTime = 0;
  
  	/**
  	 * Process listeners.
  	 */
  	protected final List<INodejsProcessListener> listeners;
  
  	/**
  	 * Lock used to wait the start of the server to retrieve port in the getPort
  	 * method.
  	 */
  	protected final Object lock = new Object();
  
  	private boolean hasError;
  
  	/**
  	 * Nodejs process constructor.
  	 * 
  	 * @param nodejsBaseDir
  	 *            the node.js base dir.
  	 * @param nodejsTernBaseDir
  	 *            the node.js tern base dir.
  	 * @param projectDir
  	 *            the project base dir where .tern-project is hosted.
  	 * @throws TernException
  	 */
  	public AbstractNodejsProcess(File nodejsBaseDir, File projectDir) throws TernException {
  		this.projectDir = projectDir;
  		this.nodejsBaseDir = nodejsBaseDir;
  		this.listeners = new ArrayList<INodejsProcessListener>();
  		this.hasError = false;
  	}
  
  	/**
  	 * Start the process and returns the port of the started process.
  	 * 
  	 * @param timeout
  	 *            to wait until the process start to retrieve the port to
  	 *            return.
  	 * @return
  	 * @throws InterruptedException
  	 * @throws IOException
  	 * @throws TernException
  	 */
  	public int start(long timeout, int testNumber) throws NodejsProcessException, InterruptedException {
  		if (!isStarted()) {
  			start();
  		}
  		waitOnStartNodejs(timeout, testNumber);
  		return getPort();
  	}
  
  	/**
  	 * Wait until node.js process is started.
  	 * 
  	 * @param timeout
  	 *            to wait until the process start to retrieve the port to
  	 *            return.
  	 * @param testNumber
  	 *            number of test to do to wait until timeout ms.
  	 * @throws InterruptedException
  	 *             throw this exception if node.js process cannot be started.
  	 * @throws NodejsProcessException
  	 *             throw this exception if node.js process cannot be started.
  	 */
  	private void waitOnStartNodejs(long timeout, int testNumber) throws InterruptedException, NodejsProcessException {
  		if (port == null) {
  			// node.js process is not started, loop for test number and wait on
  			// timeout.
  			if (!hasError) {
  				for (int i = 0; i < testNumber; i++) {
  					synchronized (lock) {
  						// wait until timeout.
  						lock.wait(timeout);
  						if (port != null || hasError) {
  							// here node.js is started, stop teh wait.
  							break;
  						}
  					}
  				}
  			}
  			if (port == null) {
  				// here node.js cannot be started.
  				throw new NodejsProcessException("Cannot start node process.");
  			}
  		}
  	}
  
  	/**
  	 * Return the node.js port and null if not started.
  	 * 
  	 * @return
  	 */
  	public Integer getPort() {
  		return port;
  	}
  
  	/**
  	 * Set the port when process is started;
  	 * 
  	 * @param port
  	 */
  	protected void setPort(Integer port) {
  		this.port = port;
  	}
  
  	/**
  	 * return the project dir where .tern-project is hosted.
  	 * 
  	 * @return
  	 */
  	public File getProjectDir() {
  		return projectDir;
  	}
  
  	/**
  	 * Returns the elapsed time to start node.js process.
  	 * 
  	 * @return
  	 */
  	public long getElapsedStartTime() {
  		return elapsedSartTime;
  	}
  
  	/**
  	 * Add the given process listener.
  	 * 
  	 * @param listener
  	 */
  	public void addProcessListener(INodejsProcessListener listener) {
  		synchronized (listeners) {
  			listeners.add(listener);
  		}
  	}
  
  	/**
  	 * Remove the given process listener.
  	 * 
  	 * @param listener
  	 */
  	public void removeProcessListener(INodejsProcessListener listener) {
  		synchronized (listeners) {
  			listeners.remove(listener);
  		}
  	}
  
  	/**
  	 * Notify start process.
  	 */
  	protected void notifyCreateProcess(List<String> commands, File projectDir) {
  		synchronized (listeners) {
  			for (INodejsProcessListener listener : listeners) {
  				listener.onCreate(this, commands, projectDir);
  			}
  		}
  	}
  
  	/**
  	 * Notify start process.
  	 * 
  	 * @param startTime
  	 *            time when node.js process is started.
  	 */
  	protected void notifyStartProcess(long startTime) {
  		this.elapsedSartTime = NodejsTernHelper.getElapsedTimeInMs(startTime);
  		synchronized (listeners) {
  			for (INodejsProcessListener listener : listeners) {
  				listener.onStart(this);
  			}
  		}
  	}
  
  	/**
  	 * Notify stop process.
  	 */
  	protected void notifyStopProcess() {
  		synchronized (listeners) {
  			for (INodejsProcessListener listener : listeners) {
  				listener.onStop(this);
  			}
  		}
  	}
  
  	/**
  	 * Notify data process.
  	 * 
  	 * @param line
  	 */
  	protected void notifyDataProcess(String line) {
  		synchronized (listeners) {
  			for (INodejsProcessListener listener : listeners) {
  				listener.onData(this, line);
  			}
  		}
  	}
  
  	/**
  	 * Notify error process.
  	 */
  	protected void notifyErrorProcess(String line) {
  		this.hasError = true;
  		synchronized (listeners) {
  			for (INodejsProcessListener listener : listeners) {
  				listener.onError(AbstractNodejsProcess.this, line);
  			}
  		}
  	}
  
  	protected List<String> createNodejsArgs() throws NodejsProcessException {
  		return getLaunchConfiguration().createNodeArgs();
  	}
  
  	protected INodejsLaunchConfiguration getLaunchConfiguration() throws NodejsProcessException {
  		if (launchConfiguration == null) {
  			throw new NodejsProcessException("Launch configuration cannot be null.");
  		}
  		return launchConfiguration;
  	}
  
  	@Override
  	public void setLaunchConfiguration(INodejsLaunchConfiguration launchConfiguration) {
  		this.launchConfiguration = launchConfiguration;
  	}
  }
  