/**
* Copyright (c) 2013-2014 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@genutiec.com> - asynchronous request processing and
- * refactoring of collectors API
*/
package tern.server.nodejs;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import javax.websocket.ClientEndpoint;
import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import tern.ITernProject;
import tern.TernException;
import tern.TernResourcesManager;
import tern.server.AbstractTernServer;
import tern.server.IInterceptor;
import tern.server.IResponseHandler;
import tern.server.TernPlugin;
import tern.server.WebSocketContainerProvider;
import tern.server.nodejs.process.INodejsLaunchConfiguration;
import tern.server.nodejs.process.INodejsProcess;
import tern.server.nodejs.process.INodejsProcessListener;
import tern.server.nodejs.process.NodejsProcessAdapter;
import tern.server.nodejs.process.NodejsProcessException;
import tern.server.nodejs.process.NodejsProcessManager;
import tern.server.nodejs.process.internal.NodejsProcess;
import tern.server.protocol.IJSONObjectHelper;
import tern.server.protocol.MinimalJSONHelper;
import tern.server.protocol.TernDoc;
import tern.server.protocol.html.ScriptTagRegion;
/**
* Tern server implemented with node.js
*
*/
public class NodejsTernServer extends AbstractTernServer implements INodejsLaunchConfiguration {
private static final String BASE_URL = "http://127.0.0.1:";
private static final String HTTP_PROTOCOL = "http:";
private static final String WS_PROTOCOL = "ws:";
private String baseURL;
private List<IInterceptor> interceptors;
private INodejsProcess process;
private List<INodejsProcessListener> listeners;
private long timeout = NodejsTernHelper.DEFAULT_TIMEOUT;
private int testNumber = NodejsTernHelper.DEFAULT_TEST_NUMBER;
/**
* Port of the node.js server.
*/
private Integer port;
/**
* true if tern server must be verbose and false otherwise.
*/
private boolean verbose;
/**
* true if tern server server don't write a .tern-port file and false
* otherwise.
*/
private boolean noPortFile = true;
/**
* false if the server will shut itself down after five minutes of
* inactivity and true otherwise.
*/
private boolean persistent;
/**
* true if tern plugins can be loaded from the project root and false
* otherwise
*/
private boolean loadingLocalPlugins;
private boolean isDebugLaunch;
private boolean isSaveLaunch;
private final INodejsProcessListener listener = new NodejsProcessAdapter() {
@Override
public void onStart(INodejsProcess server) {
NodejsTernServer.this.fireStartServer();
// initialize WebSocket client session if "push" tern plugin is
// declared in the tern-project.
initializeWebSocketIfNeeded();
}
private void initializeWebSocketIfNeeded() {
ITernProject project = getProject();
if (project == null) {
return;
}
if (!project.hasPlugin(TernPlugin.push)) {
return;
}
try {
String baseURL = getBaseURL();
URI uri = URI.create(baseURL.replace(HTTP_PROTOCOL, WS_PROTOCOL));
WebSocketContainer container = WebSocketContainerProvider.getWebSocketContainer();
session = container.connectToServer(new WebSocketMessageDispatcher(), uri);
} catch (Throwable e) {
NodejsTernServer.this.onError("Error while initializing WebSocket client.", e);
}
}
@Override
public void onStop(INodejsProcess server) {
dispose();
fireEndServer();
}
};
@ClientEndpoint
public class WebSocketMessageDispatcher {
@OnMessage
public void dispatchMessage(String data) {
JsonObject value = Json.parse(data).asObject();
String type = value.getString("type", null);
JsonValue json = value.get("data");
if (type != null && json != null) {
NodejsTernServer.this.fireOnMessage(type, json);
}
}
}
// WebSocket session
private Session session;
public NodejsTernServer(File projectDir, int port) {
this(TernResourcesManager.getTernProject(projectDir), port);
}
public NodejsTernServer(ITernProject project, int port) {
super(project);
this.baseURL = computeBaseURL(port);
}
public NodejsTernServer(ITernProject project) throws TernException {
this(project, NodejsProcessManager.getInstance().create(project.getProjectDir()));
}
public NodejsTernServer(ITernProject project, File nodejsBaseDir) throws TernException {
this(project, NodejsProcessManager.getInstance().create(project.getProjectDir(), nodejsBaseDir));
}
public NodejsTernServer(ITernProject project, File nodejsBaseDir, File nodejsTernBaseDir) throws TernException {
this(project,
NodejsProcessManager.getInstance().create(project.getProjectDir(), nodejsBaseDir, nodejsTernBaseDir));
}
public NodejsTernServer(ITernProject project, INodejsProcess process) {
super(project);
this.process = process;
process.setLaunchConfiguration(this);
process.addProcessListener(listener);
}
private String computeBaseURL(Integer port) {
return new StringBuilder(BASE_URL).append(port).append("/").toString();
}
@Override
public void addFile(String name, String text, ScriptTagRegion[] tags) {
TernDoc t = new TernDoc();
t.addFile(name, text, tags, null);
try {
makeRequest(t);
} catch (Exception e) {
onError("Error while adding file.", e);
}
}
@Override
public void request(TernDoc doc, IResponseHandler handler) {
try {
JsonObject json = makeRequest(doc);
handler.onSuccess(json, handler.isDataAsJsonString() ? json.toString() : null);
} catch (Exception e) {
handler.onError(e.getMessage(), e);
}
}
private JsonObject makeRequest(TernDoc doc) throws IOException, InterruptedException, TernException {
String baseURL = null;
try {
baseURL = getBaseURL();
if (baseURL == null) {
throw new TernException("Server has been disposed");
}
} catch (NodejsProcessException e) {
// the nodejs process cannot start => not a valid node path, dispose
// the server.
dispose();
throw e;
}
List<IInterceptor> interceptors;
beginReadState();
try {
if (this.interceptors != null) {
interceptors = new ArrayList<IInterceptor>(this.interceptors);
} else {
interceptors = null;
}
} finally {
endReadState();
}
JsonObject json = NodejsTernHelper.makeRequest(baseURL, doc, false, interceptors, this);
return json;
}
public void addInterceptor(IInterceptor interceptor) {
beginWriteState();
try {
if (interceptors == null) {
interceptors = new ArrayList<IInterceptor>();
}
interceptors.add(interceptor);
} finally {
endWriteState();
}
}
public void removeInterceptor(IInterceptor interceptor) {
beginWriteState();
try {
if (interceptors != null) {
interceptors.remove(interceptor);
}
} finally {
endWriteState();
}
}
public String getBaseURL() throws InterruptedException, TernException {
beginReadState();
try {
if (baseURL == null) {
endReadState();
beginWriteState();
try {
if (baseURL != null || isDisposed()) {// already initialized
// or disposed
return baseURL;
}
int port = getProcess().start(timeout, testNumber);
this.baseURL = computeBaseURL(port);
} finally {
endWriteState();
beginReadState();
}
}
return baseURL;
} finally {
endReadState();
}
}
private INodejsProcess getProcess() throws TernException {
if (process == null) {
ITernProject project = super.getProject();
process = NodejsProcessManager.getInstance().create(project.getProjectDir());
process.addProcessListener(listener);
}
return process;
}
public void addProcessListener(INodejsProcessListener listener) {
beginWriteState();
try {
if (listeners == null) {
listeners = new ArrayList<INodejsProcessListener>();
}
listeners.add(listener);
if (process != null) {
process.addProcessListener(listener);
}
} finally {
endWriteState();
}
}
public void removeProcessListener(INodejsProcessListener listener) {
beginWriteState();
try {
if (listeners != null && listener != null) {
listeners.remove(listener);
}
if (process != null) {
process.removeProcessListener(listener);
}
} finally {
endWriteState();
}
}
@Override
public IJSONObjectHelper getJSONObjectHelper() {
return MinimalJSONHelper.INSTANCE;
}
@Override
public void doDispose() {
beginWriteState();
try {
if (process != null) {
process.kill();
}
this.baseURL = null;
this.process = null;
if (session != null) {
try {
session.close();
} catch (Throwable e) {
onError("Error while closing WebSocket client session.", e);
}
}
} finally {
endWriteState();
}
}
/**
* Set the timeout to use when node.js starts to retrieve the node.js port
* in {@link NodejsProcess#start(long, int)} from the given project.
*/
public void setTimeout(long timeout) {
this.timeout = timeout;
}
/**
* Returns the timeout to use when node.js starts to retrieve the node.js
* port in {@link NodejsProcess#start(long, int)} from the given project.
*
* @return
*/
public long getTimeout() {
return timeout;
}
public void setTestNumber(int testNumber) {
this.testNumber = testNumber;
}
public int getTestNumber() {
return testNumber;
}
/**
* Set the verbose.
*
* @param verbose
*/
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
/**
* Returns true if tern is verbose and false otherwise.
*
* @return
*/
public boolean isVerbose() {
return verbose;
}
/**
* Set true if tern server server won't write a .tern-port file and false
* otherwise.
*
* @param noPortFile
*/
public void setNoPortFile(boolean noPortFile) {
this.noPortFile = noPortFile;
}
/**
* return true if tern server server won't write a .tern-port file and false
* otherwise.
*
* @return
*/
public boolean isNoPortFile() {
return noPortFile;
}
/**
* Set false if the server will shut itself down after five minutes of
* inactivity and true otherwise.
*
* @param persistent
*/
public void setPersistent(boolean persistent) {
this.persistent = persistent;
}
/**
* Returns false if the server will shut itself down after five minutes of
* inactivity and true otherwise.
*
* @return
*/
public boolean isPersistent() {
return persistent;
}
/**
* Set true if tern plugins can be loaded from the project root and false
* otherwise.
*
* @see https://github.com/marijnh/tern/pull/394
*/
public void setLoadingLocalPlugins(boolean loadingLocalPlugins) {
this.loadingLocalPlugins = loadingLocalPlugins;
}
/**
* Returns true if tern plugins can be loaded from the project root and
* false otherwise.
*
* @return true if tern plugins can be loaded from the project root and
* false otherwise.
* @see https://github.com/marijnh/tern/pull/394
*/
public boolean isLoadingLocalPlugins() {
return loadingLocalPlugins;
}
/**
* Return the node.js port and null if it is not a remote tern server.
*
* @return
*/
public Integer getPort() {
return port;
}
protected void onError(String message, Throwable e) {
e.printStackTrace();
}
@Override
public List<String> createNodeArgs() {
List<String> args = new LinkedList<String>();
Integer port = getPort();
if (port != null) {
args.add("--port");
args.add(port.toString());
}
if (isVerbose()) {
args.add("--verbose");
args.add("1");
}
if (isNoPortFile()) {
args.add("--no-port-file");
}
if (isPersistent()) {
args.add("--persistent");
}
if (!isLoadingLocalPlugins()) {
args.add("--disable-loading-local");
}
return args;
}
@Override
public String generateLaunchConfigurationName() {
return "tern.js for " + getProject().getProjectDir().getName();
}
@Override
public String getLaunchMode() {
return isDebugLaunch ? "debug" : "run";
}
public void setDebugLaunch(boolean isDebugLaunch) {
this.isDebugLaunch = isDebugLaunch;
}
@Override
public boolean isSaveLaunch() {
return isSaveLaunch;
}
public void setSaveLaunch(boolean isSaveLaunch) {
this.isSaveLaunch = isSaveLaunch;
}
@Override
public boolean isWaitOnPort() {
return true;
}
}