/** * 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; } }