diff options
Diffstat (limited to 'xml/impl/src/org/jetbrains/builtInWebServer')
14 files changed, 1121 insertions, 0 deletions
diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.form b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.form new file mode 100644 index 000000000000..3aa78b3be81c --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.form @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.builtInWebServer.BuiltInServerConfigurableUi"> + <grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="2" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> + <margin top="0" left="0" bottom="0" right="0"/> + <constraints> + <xy x="20" y="20" width="1077" height="321"/> + </constraints> + <properties/> + <border type="none"/> + <children> + <component id="696ad" class="javax.swing.JLabel"> + <constraints> + <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/> + </constraints> + <properties> + <text resource-bundle="messages/XmlBundle" key="setting.value.builtin.server.port.label"/> + </properties> + </component> + <component id="e47e0" class="com.intellij.ui.PortField" binding="builtInServerPort"> + <constraints> + <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false"/> + </constraints> + <properties/> + </component> + <component id="33218" class="javax.swing.JCheckBox" binding="builtInServerAvailableExternallyCheckBox" default-binding="true"> + <constraints> + <grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/> + </constraints> + <properties> + <text resource-bundle="messages/XmlBundle" key="setting.value.can.accept.external.connections"/> + </properties> + </component> + <vspacer id="c36c4"> + <constraints> + <grid row="1" column="0" row-span="1" col-span="3" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/> + </constraints> + </vspacer> + </children> + </grid> +</form> diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.java b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.java new file mode 100644 index 000000000000..edbef65543af --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.java @@ -0,0 +1,47 @@ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.options.ConfigurableUi; +import com.intellij.ui.PortField; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +class BuiltInServerConfigurableUi implements ConfigurableUi<BuiltInServerOptions> { + private JPanel mainPanel; + + private PortField builtInServerPort; + private JCheckBox builtInServerAvailableExternallyCheckBox; + + public BuiltInServerConfigurableUi() { + builtInServerPort.setMin(1024); + } + + @Override + @NotNull + public JComponent getComponent() { + return mainPanel; + } + + @Override + public boolean isModified(@NotNull BuiltInServerOptions settings) { + return builtInServerPort.getNumber() != settings.builtInServerPort || + builtInServerAvailableExternallyCheckBox.isSelected() != settings.builtInServerAvailableExternally; + } + + @Override + public void apply(@NotNull BuiltInServerOptions settings) { + boolean builtInServerPortChanged = settings.builtInServerPort != builtInServerPort.getNumber() || settings.builtInServerAvailableExternally != builtInServerAvailableExternallyCheckBox.isSelected(); + if (builtInServerPortChanged) { + settings.builtInServerPort = builtInServerPort.getNumber(); + settings.builtInServerAvailableExternally = builtInServerAvailableExternallyCheckBox.isSelected(); + + BuiltInServerOptions.onBuiltInServerPortChanged(); + } + } + + @Override + public void reset(@NotNull BuiltInServerOptions settings) { + builtInServerPort.setNumber(settings.builtInServerPort); + builtInServerAvailableExternallyCheckBox.setSelected(settings.builtInServerAvailableExternally); + } +} diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInServerOptions.java b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInServerOptions.java new file mode 100644 index 000000000000..5eb65c1f9cbc --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInServerOptions.java @@ -0,0 +1,116 @@ +package org.jetbrains.builtInWebServer; + +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationDisplayType; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.application.ApplicationNamesInfo; +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.components.*; +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.SimpleConfigurable; +import com.intellij.openapi.util.Getter; +import com.intellij.util.xmlb.XmlSerializerUtil; +import com.intellij.util.xmlb.annotations.Attribute; +import com.intellij.xdebugger.settings.DebuggerConfigurableProvider; +import com.intellij.xdebugger.settings.DebuggerSettingsCategory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.ide.BuiltInServerManager; +import org.jetbrains.ide.CustomPortServerManager; +import org.jetbrains.io.CustomPortServerManagerBase; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; + +@State( + name = "BuiltInServerOptions", + storages = { + @Storage( + file = StoragePathMacros.APP_CONFIG + "/other.xml" + )} +) +public class BuiltInServerOptions implements PersistentStateComponent<BuiltInServerOptions>, ExportableComponent, Getter<BuiltInServerOptions> { + @Attribute + public int builtInServerPort = 63342; + @Attribute + public boolean builtInServerAvailableExternally = false; + + public static BuiltInServerOptions getInstance() { + return ServiceManager.getService(BuiltInServerOptions.class); + } + + @Override + public BuiltInServerOptions get() { + return this; + } + + static final class BuiltInServerDebuggerConfigurableProvider extends DebuggerConfigurableProvider { + @NotNull + @Override + public Collection<? extends Configurable> getConfigurables(@NotNull DebuggerSettingsCategory category) { + if (category == DebuggerSettingsCategory.GENERAL) { + return Collections.singletonList(SimpleConfigurable.create("builtInServer", "", BuiltInServerConfigurableUi.class, getInstance())); + } + return Collections.emptyList(); + } + } + + @NotNull + @Override + public File[] getExportFiles() { + return new File[]{PathManager.getOptionsFile("other")}; + } + + @NotNull + @Override + public String getPresentableName() { + return "Built-in server"; + } + + @Nullable + @Override + public BuiltInServerOptions getState() { + return this; + } + + @Override + public void loadState(BuiltInServerOptions state) { + XmlSerializerUtil.copyBean(state, this); + } + + public int getEffectiveBuiltInServerPort() { + MyCustomPortServerManager portServerManager = CustomPortServerManager.EP_NAME.findExtension(MyCustomPortServerManager.class); + if (!portServerManager.isBound()) { + return BuiltInServerManager.getInstance().getPort(); + } + return builtInServerPort; + } + + public static final class MyCustomPortServerManager extends CustomPortServerManagerBase { + @Override + public void cannotBind(Exception e, int port) { + String groupDisplayId = "Built-in Web Server"; + Notifications.Bus.register(groupDisplayId, NotificationDisplayType.STICKY_BALLOON); + new Notification(groupDisplayId, "Built-in HTTP server on custom port " + port + " disabled", + "Cannot start built-in HTTP server on custom port " + port + ". " + + "Please ensure that port is free (or check your firewall settings) and restart " + ApplicationNamesInfo.getInstance().getFullProductName(), + NotificationType.ERROR).notify(null); + } + + @Override + public int getPort() { + return getInstance().builtInServerPort; + } + + @Override + public boolean isAvailableExternally() { + return getInstance().builtInServerAvailableExternally; + } + } + + public static void onBuiltInServerPortChanged() { + CustomPortServerManager.EP_NAME.findExtension(MyCustomPortServerManager.class).portChanged(); + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java new file mode 100644 index 000000000000..f58d42587cab --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java @@ -0,0 +1,78 @@ +package org.jetbrains.builtInWebServer; + +import com.intellij.ide.browsers.OpenInBrowserRequest; +import com.intellij.ide.browsers.WebBrowserUrlProvider; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.intellij.testFramework.LightVirtualFile; +import com.intellij.util.Url; +import com.intellij.util.Urls; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.xml.util.HtmlUtil; +import org.jetbrains.ide.BuiltInServerManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implements DumbAware { + @NotNull + public static List<Url> getUrls(@NotNull VirtualFile file, @NotNull Project project, @Nullable String currentAuthority) { + if (currentAuthority != null && !compareAuthority(currentAuthority)) { + return Collections.emptyList(); + } + + String path = WebServerPathToFileManager.getInstance(project).getPath(file); + if (path == null) { + return Collections.emptyList(); + } + + int effectiveBuiltInServerPort = BuiltInServerOptions.getInstance().getEffectiveBuiltInServerPort(); + Url url = Urls.newHttpUrl(currentAuthority == null ? "localhost:" + effectiveBuiltInServerPort : currentAuthority, '/' + project.getName() + '/' + path); + int defaultPort = BuiltInServerManager.getInstance().getPort(); + if (currentAuthority != null || defaultPort == effectiveBuiltInServerPort) { + return Collections.singletonList(url); + } + return Arrays.asList(url, Urls.newHttpUrl("localhost:" + defaultPort, '/' + project.getName() + '/' + path)); + } + + public static boolean compareAuthority(@Nullable String currentAuthority) { + if (currentAuthority == null) { + return false; + } + + int portIndex = currentAuthority.indexOf(':'); + if (portIndex < 0) { + return false; + } + + String host = currentAuthority.substring(0, portIndex); + if (!BuiltInWebServer.isOwnHostName(host)) { + return false; + } + + int port = StringUtil.parseInt(currentAuthority.substring(portIndex + 1), -1); + return port == BuiltInServerOptions.getInstance().getEffectiveBuiltInServerPort() || + port == BuiltInServerManager.getInstance().getPort(); + } + + @Override + public boolean canHandleElement(@NotNull OpenInBrowserRequest request) { + return request.getFile().getViewProvider().isPhysical() && !(request.getVirtualFile() instanceof LightVirtualFile) && isMyLanguage(request.getFile()); + } + + protected boolean isMyLanguage(PsiFile psiFile) { + return HtmlUtil.isHtmlFile(psiFile); + } + + @Nullable + @Override + protected Url getUrl(@NotNull OpenInBrowserRequest request, @NotNull VirtualFile virtualFile) throws BrowserException { + return ContainerUtil.getFirstItem(getUrls(virtualFile, request.getProject(), null)); + } +} diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java new file mode 100644 index 000000000000..2dc24ba01706 --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java @@ -0,0 +1,231 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.util.SystemInfoRt; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.UriUtil; +import com.intellij.util.io.URLUtil; +import com.intellij.util.net.NetUtils; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.ide.HttpRequestHandler; +import org.jetbrains.io.FileResponses; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import static org.jetbrains.io.Responses.sendOptionsResponse; +import static org.jetbrains.io.Responses.sendStatus; + +public final class BuiltInWebServer extends HttpRequestHandler { + static final Logger LOG = Logger.getInstance(BuiltInWebServer.class); + + @Nullable + public static VirtualFile findIndexFile(@NotNull VirtualFile basedir) { + VirtualFile[] children = basedir.getChildren(); + if (children == null || children.length == 0) { + return null; + } + + for (String indexNamePrefix : new String[]{"index.", "default."}) { + VirtualFile index = null; + String preferredName = indexNamePrefix + "html"; + for (VirtualFile child : children) { + if (!child.isDirectory()) { + String name = child.getName(); + if (name.equals(preferredName)) { + return child; + } + else if (index == null && name.startsWith(indexNamePrefix)) { + index = child; + } + } + } + if (index != null) { + return index; + } + } + return null; + } + + @Override + public boolean isSupported(@NotNull FullHttpRequest request) { + return super.isSupported(request) || request.method() == HttpMethod.POST || request.method() == HttpMethod.OPTIONS; + } + + @Override + public boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) { + if (request.method() == HttpMethod.OPTIONS) { + sendOptionsResponse("GET, POST, HEAD, OPTIONS", request, context); + return true; + } + + String host = HttpHeaders.getHost(request); + if (StringUtil.isEmpty(host)) { + return false; + } + + int portIndex = host.indexOf(':'); + if (portIndex > 0) { + host = host.substring(0, portIndex); + } + + String projectName; + boolean isIpv6 = host.charAt(0) == '[' && host.length() > 2 && host.charAt(host.length() - 1) == ']'; + if (isIpv6) { + host = host.substring(1, host.length() - 1); + } + + if (isIpv6 || Character.digit(host.charAt(0), 10) != -1 || host.charAt(0) == ':' || isOwnHostName(host)) { + if (urlDecoder.path().length() < 2) { + return false; + } + projectName = null; + } + else { + projectName = host; + } + return doProcess(request, context.channel(), projectName); + } + + public static boolean isOwnHostName(@NotNull String host) { + if (NetUtils.isLocalhost(host)) { + return true; + } + + try { + InetAddress address = InetAddress.getByName(host); + if (host.equals(address.getHostAddress()) || host.equalsIgnoreCase(address.getCanonicalHostName())) { + return true; + } + + String localHostName = InetAddress.getLocalHost().getHostName(); + // WEB-8889 + // develar.local is own host name: develar. equals to "develar.labs.intellij.net" (canonical host name) + return localHostName.equalsIgnoreCase(host) || + (host.endsWith(".local") && localHostName.regionMatches(true, 0, host, 0, host.length() - ".local".length())); + } + catch (UnknownHostException ignored) { + return false; + } + } + + private static boolean doProcess(@NotNull FullHttpRequest request, @NotNull Channel channel, @Nullable String projectName) { + final String decodedPath = URLUtil.unescapePercentSequences(UriUtil.trimParameters(request.uri())); + int offset; + boolean emptyPath; + boolean isCustomHost = projectName != null; + if (isCustomHost) { + // host mapped to us + offset = 0; + emptyPath = decodedPath.isEmpty(); + } + else { + offset = decodedPath.indexOf('/', 1); + projectName = decodedPath.substring(1, offset == -1 ? decodedPath.length() : offset); + emptyPath = offset == -1; + } + + Project project = findProject(projectName, isCustomHost); + if (project == null) { + return false; + } + + if (emptyPath) { + if (!SystemInfoRt.isFileSystemCaseSensitive) { + // may be passed path is not correct + projectName = project.getName(); + } + + // we must redirect "jsdebug" to "jsdebug/" as nginx does, otherwise browser will treat it as file instead of directory, so, relative path will not work + WebServerPathHandler.redirectToDirectory(request, channel, projectName); + return true; + } + + final String path = FileUtil.toCanonicalPath(decodedPath.substring(offset + 1), '/'); + LOG.assertTrue(path != null); + + for (WebServerPathHandler pathHandler : WebServerPathHandler.EP_NAME.getExtensions()) { + try { + if (pathHandler.process(path, project, request, channel, projectName, decodedPath, isCustomHost)) { + return true; + } + } + catch (Throwable e) { + LOG.error(e); + } + } + return false; + } + + static final class StaticFileHandler extends WebServerFileHandler { + @Override + public boolean process(@NotNull VirtualFile file, + @NotNull CharSequence canonicalRequestPath, + @NotNull Project project, + @NotNull FullHttpRequest request, + @NotNull Channel channel) throws IOException { + File ioFile = VfsUtilCore.virtualToIoFile(file); + if (hasAccess(ioFile)) { + FileResponses.sendFile(request, channel, ioFile); + } + else { + sendStatus(HttpResponseStatus.FORBIDDEN, channel, request); + } + return true; + } + + private static boolean hasAccess(File result) { + // deny access to .htaccess files + return !result.isDirectory() && result.canRead() && !(result.isHidden() || result.getName().startsWith(".ht")); + } + } + + @Nullable + private static Project findProject(String projectName, boolean isCustomHost) { + // user can rename project directory, so, we should support this case - find project by base directory name + Project candidateByDirectoryName = null; + for (Project project : ProjectManager.getInstance().getOpenProjects()) { + String name = project.getName(); + // domain name is case-insensitive + if (!project.isDisposed() && ((isCustomHost || !SystemInfoRt.isFileSystemCaseSensitive) ? projectName.equalsIgnoreCase(name) : projectName.equals(name))) { + return project; + } + + if (candidateByDirectoryName == null && compareNameAndProjectBasePath(projectName, project)) { + candidateByDirectoryName = project; + } + } + return candidateByDirectoryName; + } + + public static boolean compareNameAndProjectBasePath(String projectName, Project project) { + String basePath = project.getBasePath(); + return basePath != null && basePath.length() > projectName.length() && basePath.endsWith(projectName) && basePath.charAt(basePath.length() - projectName.length() - 1) == '/'; + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.java b/xml/impl/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.java new file mode 100644 index 000000000000..1b4acb07556f --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.java @@ -0,0 +1,102 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.io.Responses; + +final class DefaultWebServerPathHandler extends WebServerPathHandler { + @Override + public boolean process(@NotNull String path, + @NotNull Project project, + @NotNull FullHttpRequest request, + @NotNull Channel channel, + @Nullable String projectName, + @NotNull String decodedRawPath, + boolean isCustomHost) { + WebServerPathToFileManager pathToFileManager = WebServerPathToFileManager.getInstance(project); + VirtualFile result = pathToFileManager.pathToFileCache.getIfPresent(path); + boolean indexUsed = false; + if (result == null || !result.isValid()) { + result = pathToFileManager.findByRelativePath(project, path); + if (result == null) { + if (path.isEmpty()) { + Responses.sendStatus(HttpResponseStatus.NOT_FOUND, channel, "Index file doesn't exist.", request); + return true; + } + else { + return false; + } + } + else if (result.isDirectory()) { + if (!endsWithSlash(decodedRawPath)) { + redirectToDirectory(request, channel, isCustomHost ? path : (projectName + '/' + path)); + return true; + } + + result = BuiltInWebServer.findIndexFile(result); + if (result == null) { + Responses.sendStatus(HttpResponseStatus.NOT_FOUND, channel, "Index file doesn't exist.", request); + return true; + } + indexUsed = true; + } + + pathToFileManager.pathToFileCache.put(path, result); + } + else if (!path.endsWith(result.getName())) { + if (endsWithSlash(decodedRawPath)) { + indexUsed = true; + } + else { + // FallbackResource feature in action, /login requested, /index.php retrieved, we must not redirect /login to /login/ + if (path.endsWith(result.getParent().getName())) { + redirectToDirectory(request, channel, isCustomHost ? path : (projectName + '/' + path)); + return true; + } + } + } + + StringBuilder canonicalRequestPath = new StringBuilder(); + canonicalRequestPath.append('/'); + if (!isCustomHost) { + canonicalRequestPath.append(projectName).append('/'); + } + canonicalRequestPath.append(path); + if (indexUsed) { + canonicalRequestPath.append('/').append(result.getName()); + } + + for (WebServerFileHandler fileHandler : WebServerFileHandler.EP_NAME.getExtensions()) { + try { + if (fileHandler.process(result, canonicalRequestPath, project, request, channel)) { + return true; + } + } + catch (Throwable e) { + BuiltInWebServer.LOG.error(e); + } + } + + return false; + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/DefaultWebServerRootsProvider.java b/xml/impl/src/org/jetbrains/builtInWebServer/DefaultWebServerRootsProvider.java new file mode 100644 index 000000000000..b5621295957d --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/DefaultWebServerRootsProvider.java @@ -0,0 +1,150 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.application.AccessToken; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.ProjectFileIndex; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.PairFunction; +import com.intellij.util.PlatformUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class DefaultWebServerRootsProvider extends WebServerRootsProvider { + @Nullable + @Override + public PathInfo resolve(@NotNull String path, @NotNull Project project) { + PairFunction<String, VirtualFile, VirtualFile> resolver; + if (PlatformUtils.isIntelliJ()) { + int index = path.indexOf('/'); + if (index > 0 && !path.regionMatches(!SystemInfo.isFileSystemCaseSensitive, 0, project.getName(), 0, index)) { + String moduleName = path.substring(0, index); + AccessToken token = ReadAction.start(); + Module module; + try { + module = ModuleManager.getInstance(project).findModuleByName(moduleName); + } + finally { + token.finish(); + } + + if (module != null && !module.isDisposed()) { + path = path.substring(index + 1); + resolver = WebServerPathToFileManager.getInstance(project).getResolver(path); + + ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module); + PathInfo result = resolve(path, moduleRootManager.getSourceRoots(), resolver, moduleName); + if (result == null) { + result = resolve(path, moduleRootManager.getContentRoots(), resolver, moduleName); + } + if (result != null) { + return result; + } + } + } + } + + Module[] modules; + AccessToken token = ReadAction.start(); + try { + modules = ModuleManager.getInstance(project).getModules(); + } + finally { + token.finish(); + } + + resolver = WebServerPathToFileManager.getInstance(project).getResolver(path); + PathInfo result = findByRelativePath(project, path, modules, true, resolver); + if (result == null) { + // let's find in content roots + return findByRelativePath(project, path, modules, false, resolver); + } + else { + return result; + } + } + + @Nullable + @Override + public PathInfo getRoot(@NotNull VirtualFile file, @NotNull Project project) { + AccessToken token = ReadAction.start(); + try { + VirtualFile root = null; + ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex(); + if (fileIndex.isInSourceContent(file)) { + root = fileIndex.getSourceRootForFile(file); + } + else if (fileIndex.isInContent(file)) { + root = fileIndex.getContentRootForFile(file); + } + else if (fileIndex.isInLibraryClasses(file)) { + root = fileIndex.getClassRootForFile(file); + } + assert root != null : file.getPresentableUrl(); + return new PathInfo(file, root, getModuleNameQualifier(project, fileIndex.getModuleForFile(file))); + } + finally { + token.finish(); + } + } + + @Nullable + private static String getModuleNameQualifier(@NotNull Project project, @Nullable Module module) { + if (module != null && + PlatformUtils.isIntelliJ() && + !(module.getName().equalsIgnoreCase(project.getName()) || BuiltInWebServer.compareNameAndProjectBasePath(module.getName(), project))) { + return module.getName(); + } + return null; + } + + @Nullable + private static PathInfo resolve(@NotNull String path, @NotNull VirtualFile[] roots, @NotNull PairFunction<String, VirtualFile, VirtualFile> resolver, @Nullable String moduleName) { + for (VirtualFile root : roots) { + VirtualFile file = resolver.fun(path, root); + if (file != null) { + return new PathInfo(file, root, moduleName); + } + } + return null; + } + + @Nullable + private static PathInfo findByRelativePath(@NotNull Project project, + @NotNull String path, + @NotNull Module[] modules, + boolean inSourceRoot, + @NotNull PairFunction<String, VirtualFile, VirtualFile> resolver) { + for (Module module : modules) { + if (!module.isDisposed()) { + ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module); + PathInfo result = resolve(path, inSourceRoot ? moduleRootManager.getSourceRoots() : moduleRootManager.getContentRoots(), resolver, null); + if (result != null) { + result.moduleName = getModuleNameQualifier(project, module); + return result; + } + } + } + return null; + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/PathInfo.java b/xml/impl/src/org/jetbrains/builtInWebServer/PathInfo.java new file mode 100644 index 000000000000..b55b16be84b2 --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/PathInfo.java @@ -0,0 +1,47 @@ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class PathInfo { + private final VirtualFile child; + private final VirtualFile root; + String moduleName; + + private String computedPath; + + public PathInfo(@NotNull VirtualFile child, @NotNull VirtualFile root, @Nullable String moduleName) { + this.child = child; + this.root = root; + this.moduleName = moduleName; + } + + public PathInfo(@NotNull VirtualFile child, @NotNull VirtualFile root) { + this(child, root, null); + } + + @NotNull + public VirtualFile getChild() { + return child; + } + + @NotNull + public VirtualFile getRoot() { + return root; + } + + @Nullable + public String getModuleName() { + return moduleName; + } + + @NotNull + public String getPath() { + if (computedPath == null) { + computedPath = (moduleName == null ? "" : moduleName + '/') + VfsUtilCore.getRelativePath(child, root, '/'); + } + return computedPath; + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/PrefixlessWebServerRootsProvider.java b/xml/impl/src/org/jetbrains/builtInWebServer/PrefixlessWebServerRootsProvider.java new file mode 100644 index 000000000000..145fdedb350e --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/PrefixlessWebServerRootsProvider.java @@ -0,0 +1,18 @@ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.PairFunction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class PrefixlessWebServerRootsProvider extends WebServerRootsProvider { + @Nullable + @Override + public final PathInfo resolve(@NotNull String path, @NotNull Project project) { + return resolve(path, project, WebServerPathToFileManager.getInstance(project).getResolver(path)); + } + + @Nullable + public abstract PathInfo resolve(@NotNull String path, @NotNull Project project, @NotNull PairFunction<String, VirtualFile, VirtualFile> resolver); +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/WebServerFileHandler.java b/xml/impl/src/org/jetbrains/builtInWebServer/WebServerFileHandler.java new file mode 100644 index 000000000000..9035af6c52cc --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/WebServerFileHandler.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.FullHttpRequest; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +public abstract class WebServerFileHandler { + static final ExtensionPointName<WebServerFileHandler> EP_NAME = ExtensionPointName.create("org.jetbrains.webServerFileHandler"); + + public abstract boolean process(@NotNull VirtualFile file, + @NotNull CharSequence canonicalRequestPath, + @NotNull Project project, + @NotNull FullHttpRequest request, + @NotNull Channel channel) throws IOException; +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/WebServerPathHandler.java b/xml/impl/src/org/jetbrains/builtInWebServer/WebServerPathHandler.java new file mode 100644 index 000000000000..807c3b31b49a --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/WebServerPathHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VfsUtil; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.io.Responses; + +import java.net.URI; + +/** + * By default {@link WebServerPathToFileManager} will be used to map request to file. + * If file physically exists in the file system, you must use {@link WebServerRootsProvider}. + * + * Consider to extend {@link WebServerPathHandlerAdapter} instead of implement low-level {@link #process(String, com.intellij.openapi.project.Project, io.netty.handler.codec.http.FullHttpRequest, io.netty.channel.Channel, String, String, boolean)} + */ +public abstract class WebServerPathHandler { + static final ExtensionPointName<WebServerPathHandler> EP_NAME = ExtensionPointName.create("org.jetbrains.webServerPathHandler"); + + public abstract boolean process(@NotNull String path, + @NotNull Project project, + @NotNull FullHttpRequest request, + @NotNull Channel channel, + @Nullable String projectName, + @NotNull String decodedRawPath, + boolean isCustomHost); + + protected static void redirectToDirectory(@NotNull HttpRequest request, @NotNull Channel channel, @NotNull String path) { + FullHttpResponse response = Responses.response(HttpResponseStatus.MOVED_PERMANENTLY); + URI url = VfsUtil.toUri("http://" + HttpHeaders.getHost(request) + '/' + path + '/'); + BuiltInWebServer.LOG.assertTrue(url != null); + response.headers().add(HttpHeaders.Names.LOCATION, url.toASCIIString()); + Responses.send(response, channel, request); + } + + protected static boolean endsWithSlash(@NotNull String path) { + return path.charAt(path.length() - 1) == '/'; + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/WebServerPathHandlerAdapter.java b/xml/impl/src/org/jetbrains/builtInWebServer/WebServerPathHandlerAdapter.java new file mode 100644 index 000000000000..a1f1e1790195 --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/WebServerPathHandlerAdapter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.project.Project; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.FullHttpRequest; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class WebServerPathHandlerAdapter extends WebServerPathHandler { + protected abstract boolean process(@NotNull String path, @NotNull Project project, @NotNull FullHttpRequest request, @NotNull Channel channel); + + @Override + public final boolean process(@NotNull String path, + @NotNull Project project, + @NotNull FullHttpRequest request, + @NotNull Channel channel, + @Nullable String projectName, + @NotNull String decodedRawPath, + boolean isCustomHost) { + return process(path, project, request, channel); + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/WebServerPathToFileManager.java b/xml/impl/src/org/jetbrains/builtInWebServer/WebServerPathToFileManager.java new file mode 100644 index 000000000000..987fdcd3a139 --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/WebServerPathToFileManager.java @@ -0,0 +1,142 @@ +package org.jetbrains.builtInWebServer; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.intellij.ProjectTopics; +import com.intellij.openapi.application.Application; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ModuleRootAdapter; +import com.intellij.openapi.roots.ModuleRootEvent; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.openapi.vfs.newvfs.BulkFileListener; +import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent; +import com.intellij.openapi.vfs.newvfs.events.VFileEvent; +import com.intellij.util.PairFunction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Implement {@link WebServerRootsProvider} to add your provider + */ +public class WebServerPathToFileManager { + private static final PairFunction<String, VirtualFile, VirtualFile> RELATIVE_PATH_RESOLVER = new PairFunction<String, VirtualFile, VirtualFile>() { + @Nullable + @Override + public VirtualFile fun(String path, VirtualFile parent) { + return parent.findFileByRelativePath(path); + } + }; + + private static final PairFunction<String, VirtualFile, VirtualFile> EMPTY_PATH_RESOLVER = new PairFunction<String, VirtualFile, VirtualFile>() { + @Nullable + @Override + public VirtualFile fun(String path, VirtualFile parent) { + return BuiltInWebServer.findIndexFile(parent); + } + }; + + private final Project project; + + final Cache<String, VirtualFile> pathToFileCache = CacheBuilder.newBuilder().maximumSize(512).expireAfterAccess(10, TimeUnit.MINUTES).build(); + // time to expire should be greater than pathToFileCache + private final Cache<VirtualFile, PathInfo> fileToRoot = CacheBuilder.newBuilder().maximumSize(512).expireAfterAccess(11, TimeUnit.MINUTES).build(); + + public static WebServerPathToFileManager getInstance(@NotNull Project project) { + return ServiceManager.getService(project, WebServerPathToFileManager.class); + } + + public WebServerPathToFileManager(@NotNull Application application, @NotNull Project project) { + this.project = project; + application.getMessageBus().connect(project).subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener.Adapter() { + @Override + public void after(@NotNull List<? extends VFileEvent> events) { + for (VFileEvent event : events) { + if (event instanceof VFileContentChangeEvent) { + VirtualFile file = ((VFileContentChangeEvent)event).getFile(); + for (WebServerRootsProvider rootsProvider : WebServerRootsProvider.EP_NAME.getExtensions()) { + if (rootsProvider.isClearCacheOnFileContentChanged(file)) { + clearCache(); + break; + } + } + } + else { + clearCache(); + break; + } + } + } + }); + project.getMessageBus().connect().subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() { + @Override + public void rootsChanged(ModuleRootEvent event) { + clearCache(); + } + }); + } + + private void clearCache() { + pathToFileCache.invalidateAll(); + fileToRoot.invalidateAll(); + } + + @Nullable + public VirtualFile get(@NotNull String path) { + return get(path, true); + } + + @Nullable + public VirtualFile get(@NotNull String path, boolean cacheResult) { + VirtualFile result = pathToFileCache.getIfPresent(path); + if (result == null || !result.isValid()) { + result = findByRelativePath(project, path); + if (cacheResult && result != null && result.isValid()) { + pathToFileCache.put(path, result); + } + } + return result; + } + + @Nullable + public String getPath(@NotNull VirtualFile file) { + PathInfo pathInfo = getRoot(file); + return pathInfo == null ? null : pathInfo.getPath(); + } + + @Nullable + public PathInfo getRoot(@NotNull VirtualFile child) { + PathInfo result = fileToRoot.getIfPresent(child); + if (result == null) { + for (WebServerRootsProvider rootsProvider : WebServerRootsProvider.EP_NAME.getExtensions()) { + result = rootsProvider.getRoot(child, project); + if (result != null) { + fileToRoot.put(child, result); + break; + } + } + } + return result; + } + + @Nullable + VirtualFile findByRelativePath(@NotNull Project project, @NotNull String path) { + for (WebServerRootsProvider rootsProvider : WebServerRootsProvider.EP_NAME.getExtensions()) { + PathInfo result = rootsProvider.resolve(path, project); + if (result != null) { + fileToRoot.put(result.getChild(), result); + return result.getChild(); + } + } + return null; + } + + @NotNull + public PairFunction<String, VirtualFile, VirtualFile> getResolver(@NotNull String path) { + return path.isEmpty() ? EMPTY_PATH_RESOLVER : RELATIVE_PATH_RESOLVER; + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/WebServerRootsProvider.java b/xml/impl/src/org/jetbrains/builtInWebServer/WebServerRootsProvider.java new file mode 100644 index 000000000000..bb8972591ac5 --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/WebServerRootsProvider.java @@ -0,0 +1,21 @@ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class WebServerRootsProvider { + static final ExtensionPointName<WebServerRootsProvider> EP_NAME = ExtensionPointName.create("org.jetbrains.webServerRootsProvider"); + + @Nullable + public abstract PathInfo resolve(@NotNull String path, @NotNull Project project); + + @Nullable + public abstract PathInfo getRoot(@NotNull VirtualFile file, @NotNull Project project); + + public boolean isClearCacheOnFileContentChanged(@NotNull VirtualFile file) { + return false; + } +}
\ No newline at end of file |