com.intellij.lang.jsgraphql.languageservice.JSGraphQLNodeLanguageServiceInstance.java Source code

Java tutorial

Introduction

Here is the source code for com.intellij.lang.jsgraphql.languageservice.JSGraphQLNodeLanguageServiceInstance.java

Source

/**
 *  Copyright (c) 2015-present, Jim Kynde Meyer
 *  All rights reserved.
 *
 *  This source code is licensed under the MIT license found in the
 *  LICENSE file in the root directory of this source tree.
 */
package com.intellij.lang.jsgraphql.languageservice;

import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.OSProcessHandler;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.ide.plugins.IdeaPluginDescriptor;
import com.intellij.ide.plugins.PluginManager;
import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreter;
import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager;
import com.intellij.javascript.nodejs.interpreter.local.NodeJsLocalInterpreter;
import com.intellij.lang.jsgraphql.JSGraphQLDebugUtil;
import com.intellij.lang.jsgraphql.ide.project.JSGraphQLLanguageUIProjectService;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.PluginDescriptor;
import com.intellij.openapi.extensions.PluginId;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.project.ProjectManagerListener;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.net.NetUtils;
import com.intellij.util.ui.UIUtil;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Represents a js-graphql-language-service instance running as a Node.js process handler.
 */
public class JSGraphQLNodeLanguageServiceInstance implements ProjectManagerListener {

    public static final String JSGRAPHQL_INTELLIJ_PLUGIN_ID = "com.intellij.lang.jsgraphql";
    public static final String JSGRAPHQL_LANGUAGE_SERVICE_DIST_JS = "js-graphql-language-service.dist.js";
    public static final String JSGRAPHQL_LANGUAGE_SERVICE_MAPPING = "/js-graphql-language-service";

    private static final Logger log = Logger.getInstance(JSGraphQLNodeLanguageServiceInstance.class);

    private final static Object jsGraphQLNodeLanguageServiceLock = new Object();
    private static File jsGraphQLNodeLanguageServiceFileName = null;

    private Project project;
    private URL url;
    private OSProcessHandler processHandler;
    private String schemaProjectDir;

    public JSGraphQLNodeLanguageServiceInstance(@NotNull Project project) {

        this.project = project;

        final ProjectManager projectManager = ProjectManager.getInstance();
        if (projectManager != null) {
            projectManager.addProjectManagerListener(project, this);
        }

        if (JSGraphQLDebugUtil.debug && JSGraphQLDebugUtil.languageServiceUrl != null) {
            try {
                url = new URL(JSGraphQLDebugUtil.languageServiceUrl);
                return; // debug url doesn't require us to create a process
            } catch (MalformedURLException e) {
                log.error("Invalid language service debug url", JSGraphQLDebugUtil.languageServiceUrl);
            }
        }

        try {
            createProcessHandler();
        } catch (Exception e) {
            log.error("Unable to start JS GraphQL Language Service using Node.js", e);
        }

    }

    public static String getNodeInterpreter(Project project) {
        final NodeJsInterpreter nodeJsInterpreter = NodeJsInterpreterManager.getInstance(project).getDefault();
        if (nodeJsInterpreter instanceof NodeJsLocalInterpreter) {
            return ((NodeJsLocalInterpreter) nodeJsInterpreter).getInterpreterSystemDependentPath();
        }
        return null;
    }

    private void createProcessHandler() {

        // Make sure we have a node interpreter
        final String nodeInterpreter = getNodeInterpreter(project);
        if (nodeInterpreter == null) {
            if (log.isDebugEnabled()) {
                log.debug("Can't create process handler: No Node.js interpreter configured.");
            }
            return;
        }

        try {

            if (log.isDebugEnabled()) {
                log.debug("Resolving node.js file...");
            }

            final File jsGraphQLNodeFile = getOrCreateJSGraphQLLanguageServiceFileName();
            if (jsGraphQLNodeFile == null) {
                if (log.isDebugEnabled()) {
                    log.debug(
                            "Can't create process handler: Got null from getOrCreateJSGraphQLLanguageServiceFileName.");
                }
                return;
            }

            final int socketPort = NetUtils.findAvailableSocketPort();
            final GeneralCommandLine commandLine = new GeneralCommandLine(nodeInterpreter);

            commandLine.withWorkDirectory(jsGraphQLNodeFile.getParent());
            commandLine.addParameter(jsGraphQLNodeFile.getAbsolutePath());

            commandLine.addParameter("--port=" + socketPort);

            if (log.isDebugEnabled()) {
                log.debug("Creating processHandler using command line " + commandLine.toString());
            }

            processHandler = new OSProcessHandler(commandLine);
            JSGraphQLLanguageUIProjectService languageService = JSGraphQLLanguageUIProjectService
                    .getService(project);
            final Runnable onInitialized = languageService.connectToProcessHandler(processHandler);

            if (waitForListeningNotification(processHandler, project)) {
                url = new URL("http", NetUtils.getLocalHostString(), socketPort,
                        JSGRAPHQL_LANGUAGE_SERVICE_MAPPING);
                onInitialized.run();
            } else {
                log.error("Unable to start JS GraphQL Language Service using Node.js with commandline "
                        + commandLine.toString());
            }

        } catch (IOException | ExecutionException e) {
            log.error("Error running JS GraphQL Language Service using Node.js", e);
        }
    }

    // ---- getters ----

    @NotNull
    public Project getProject() {
        return project;
    }

    public URL getUrl() {
        return url;
    }

    public String getSchemaProjectDir() {
        return schemaProjectDir;
    }

    public void setSchemaProjectDir(String schemaProjectDir) {
        this.schemaProjectDir = schemaProjectDir;
    }

    // ---- actions

    public void restart(Runnable onRestartedInvoke) {
        if (processHandler != null) {
            processHandler.destroyProcess();
            createProcessHandler();
            final Application application = ApplicationManager.getApplication();
            application.executeOnPooledThread(() -> {
                JSGraphQLNodeLanguageServiceClient.onInstanceRestarted(this);
                if (onRestartedInvoke != null) {
                    UIUtil.invokeLaterIfNeeded(onRestartedInvoke);
                }
            });
        } else {
            JSGraphQLNodeLanguageServiceClient.onInstanceRestarted(this);
            if (onRestartedInvoke != null) {
                onRestartedInvoke.run();
            }
        }
    }

    // ---- js-graphql-language-service.dist.js ----

    private File getOrCreateJSGraphQLLanguageServiceFileName() {

        synchronized (jsGraphQLNodeLanguageServiceLock) {

            if (jsGraphQLNodeLanguageServiceFileName == null) {

                if (JSGraphQLDebugUtil.debug && JSGraphQLDebugUtil.languageServiceDistFile != null) {
                    jsGraphQLNodeLanguageServiceFileName = new File(JSGraphQLDebugUtil.languageServiceDistFile);
                    return jsGraphQLNodeLanguageServiceFileName;
                }

                final IdeaPluginDescriptor pluginDescriptor = PluginManager
                        .getPlugin(PluginId.getId(JSGRAPHQL_INTELLIJ_PLUGIN_ID));
                if (pluginDescriptor != null) {
                    final String workingDir;
                    boolean isJar = !pluginDescriptor.getPath().isDirectory();
                    if (isJar) {
                        workingDir = pluginDescriptor.getPath().getParentFile().getAbsolutePath();
                    } else {
                        workingDir = pluginDescriptor.getPath().getAbsolutePath();
                    }
                    final File distJS = new File(workingDir, JSGRAPHQL_LANGUAGE_SERVICE_DIST_JS);
                    if (log.isDebugEnabled()) {
                        log.debug("Found it at " + distJS.toString());
                    }
                    if (distJS.exists()) {
                        // check if we need to write an updated version
                        if (!isJar || (pluginDescriptor.getPath().lastModified() > distJS.lastModified())) {
                            // update
                            jsGraphQLNodeLanguageServiceFileName = createDistJSFile(pluginDescriptor, distJS);
                        } else {
                            // reuse existing
                            jsGraphQLNodeLanguageServiceFileName = distJS;
                        }
                    } else {
                        // doesn't exist -- create it
                        jsGraphQLNodeLanguageServiceFileName = createDistJSFile(pluginDescriptor, distJS);
                    }
                } else {
                    if (log.isDebugEnabled()) {
                        log.debug("No plugin descriptor for " + JSGRAPHQL_INTELLIJ_PLUGIN_ID);
                    }
                }

            }

            return jsGraphQLNodeLanguageServiceFileName;
        }
    }

    private File createDistJSFile(PluginDescriptor pluginDescriptor, File distJS) {
        try {
            if (distJS.exists()) {
                if (log.isDebugEnabled()) {
                    log.debug("Deleting " + distJS + " to extract new version");
                }
                distJS.delete();
            }
            try (OutputStream stream = new FileOutputStream(distJS)) {
                try (InputStream inputStream = pluginDescriptor.getPluginClassLoader()
                        .getResourceAsStream("/META-INF/dist/" + JSGRAPHQL_LANGUAGE_SERVICE_DIST_JS)) {
                    if (inputStream != null) {
                        IOUtils.copy(inputStream, stream);
                    } else {
                        log.error("Couldn't load " + JSGRAPHQL_LANGUAGE_SERVICE_DIST_JS + " from "
                                + pluginDescriptor.getPluginClassLoader());
                    }
                }
            }
            return distJS;
        } catch (IOException e) {
            log.error("JS GraphQL: Unable to create file '" + distJS.getAbsolutePath() + "': " + e.getMessage());
        }
        return null;
    }

    // ---- Node.JS process handler ----

    private static boolean waitForListeningNotification(OSProcessHandler processHandler, Project project) {
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        final Ref<Boolean> result = new Ref<>(false);
        ProcessAdapter listener = new ProcessAdapter() {
            public void onTextAvailable(ProcessEvent event, Key outputType) {
                if (!StringUtil.isEmpty(event.getText())) {
                    if (outputType == ProcessOutputTypes.STDOUT || outputType == ProcessOutputTypes.SYSTEM) {
                        if (log.isDebugEnabled()) {
                            log.debug("Language Service response: " + event.getText());
                        }
                        final String text = event.getText().trim();
                        if (text.startsWith("JS GraphQL listening on")) {
                            result.set(true);
                            countDownLatch.countDown();
                        }
                    } else if (outputType == ProcessOutputTypes.STDERR) {
                        result.set(false);
                        countDownLatch.countDown();
                        JSGraphQLLanguageUIProjectService.showConsole(project);
                    }
                }
            }

            public void processTerminated(ProcessEvent event) {
                countDownLatch.countDown();
            }
        };
        processHandler.addProcessListener(listener);
        processHandler.startNotify();
        try {
            log.debug("Start waiting for ready start");
            countDownLatch.await(30L, TimeUnit.SECONDS);
            log.debug("End waiting for process starting. Result " + result.get());
        } catch (InterruptedException e) {
            log.debug("Process interrupted while waiting ready state", e);
        }

        processHandler.removeProcessListener(listener);
        return result.get();
    }

    // ---- project listener ----

    @Override
    public void projectClosing(Project project) {
        if (processHandler != null) {
            processHandler.destroyProcess();
        }
        JSGraphQLNodeLanguageServiceClient.onProjectClosing(this);
        this.project = null;
    }

    @Override
    public void projectClosed(Project project) {
    }

    @Override
    public void projectOpened(Project project) {
    }

    @Override
    public boolean canCloseProject(Project project) {
        return true;
    }

}