diff options
Diffstat (limited to 'xml/impl')
41 files changed, 2242 insertions, 183 deletions
diff --git a/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.form b/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.form index 01f7df00f6bd..0ddd4cf577f3 100644 --- a/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.form +++ b/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.form @@ -17,7 +17,7 @@ <border type="none"/> <children/> </xy> - <grid id="1b801" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> + <grid id="1b801" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <margin top="0" left="0" bottom="0" right="0"/> <constraints> <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> @@ -27,14 +27,14 @@ <children> <scrollpane id="3e3f1" class="com.intellij.ui.components.JBScrollPane" binding="myJBScrollPane" custom-create="true"> <constraints> - <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="0" anchor="9" fill="0" indent="0" use-parent-layout="false"/> + <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="0" anchor="9" fill="0" indent="0" use-parent-layout="false"/> </constraints> <properties> <horizontalScrollBarPolicy value="31"/> </properties> <border type="empty"/> <children> - <grid id="b4cfb" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> + <grid id="b4cfb" layout-manager="GridLayoutManager" row-count="4" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <margin top="0" left="0" bottom="0" right="0"/> <constraints/> <properties/> @@ -43,7 +43,7 @@ <grid id="ca5bb" layout-manager="GridLayoutManager" row-count="7" 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> - <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> + <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> <border type="none"/> @@ -185,7 +185,7 @@ <grid id="b8acc" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <margin top="0" left="0" bottom="0" right="0"/> <constraints> - <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> + <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> <border type="none"/> @@ -277,7 +277,7 @@ <grid id="6dece" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <margin top="0" left="0" bottom="0" right="0"/> <constraints> - <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> + <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> <border type="none"/> @@ -336,6 +336,15 @@ </component> </children> </grid> + <grid id="eb695" binding="myRightMarginPanel" custom-create="true" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> + <margin top="0" left="0" bottom="0" right="0"/> + <constraints> + <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> + </constraints> + <properties/> + <border type="none"/> + <children/> + </grid> </children> </grid> </children> diff --git a/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.java b/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.java index ba5a72a80beb..476181ff256d 100644 --- a/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.java +++ b/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.java @@ -15,6 +15,7 @@ */ package com.intellij.application.options; +import com.intellij.application.options.codeStyle.RightMarginForm; import com.intellij.ide.highlighter.XmlHighlighterFactory; import com.intellij.openapi.application.ApplicationBundle; import com.intellij.openapi.editor.colors.EditorColorsScheme; @@ -30,6 +31,7 @@ import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.intellij.ui.components.JBScrollPane; import com.intellij.util.ArrayUtil; import com.intellij.util.PlatformIcons; +import com.intellij.util.ui.Centerizer; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -64,6 +66,8 @@ public class CodeStyleHtmlPanel extends CodeStyleAbstractPanel { private JCheckBox myShouldKeepLineBreaksInText; private TextFieldWithBrowseButton myDontBreakIfInlineContent; private JBScrollPane myJBScrollPane; + private JPanel myRightMarginPanel; + private RightMarginForm myRightMarginForm; public CodeStyleHtmlPanel(CodeStyleSettings settings) { super(settings); @@ -95,6 +99,8 @@ public class CodeStyleHtmlPanel extends CodeStyleAbstractPanel { } private void createUIComponents() { + myRightMarginForm = new RightMarginForm(StdFileTypes.HTML.getLanguage(), getSettings()); + myRightMarginPanel = myRightMarginForm.getTopPanel(); myJBScrollPane = new JBScrollPane() { @Override public Dimension getPreferredSize() { @@ -158,6 +164,7 @@ public class CodeStyleHtmlPanel extends CodeStyleAbstractPanel { settings.HTML_KEEP_WHITESPACES_INSIDE = myKeepWhiteSpacesTagNames.getText(); settings.HTML_KEEP_LINE_BREAKS = myShouldKeepBlankLines.isSelected(); settings.HTML_KEEP_LINE_BREAKS_IN_TEXT = myShouldKeepLineBreaksInText.isSelected(); + myRightMarginForm.apply(settings); } private static int getIntValue(JTextField keepBlankLines) { @@ -190,6 +197,7 @@ public class CodeStyleHtmlPanel extends CodeStyleAbstractPanel { myInlineElementsTagNames.setText(settings.HTML_INLINE_ELEMENTS); myDontBreakIfInlineContent.setText(settings.HTML_DONT_ADD_BREAKS_IF_INLINE_CONTENT); myKeepWhiteSpacesTagNames.setText(settings.HTML_KEEP_WHITESPACES_INSIDE); + myRightMarginForm.reset(settings); } @Override @@ -260,7 +268,7 @@ public class CodeStyleHtmlPanel extends CodeStyleAbstractPanel { return true; } - return false; + return myRightMarginForm.isModified(settings); } @Override diff --git a/xml/impl/src/com/intellij/application/options/CodeStyleXmlPanel.form b/xml/impl/src/com/intellij/application/options/CodeStyleXmlPanel.form index 28d48d089cc1..c7895930be0f 100644 --- a/xml/impl/src/com/intellij/application/options/CodeStyleXmlPanel.form +++ b/xml/impl/src/com/intellij/application/options/CodeStyleXmlPanel.form @@ -17,7 +17,7 @@ </properties> <border type="empty"/> <children> - <grid id="265f" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> + <grid id="265f" layout-manager="GridLayoutManager" row-count="4" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <margin top="0" left="0" bottom="0" right="0"/> <constraints/> <properties/> @@ -26,7 +26,7 @@ <grid id="417cd" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <margin top="0" left="0" bottom="0" right="0"/> <constraints> - <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> + <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> <border type="none"/> @@ -114,7 +114,7 @@ <grid id="e7045" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <margin top="0" left="0" bottom="0" right="0"/> <constraints> - <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> + <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> <border type="none"/> @@ -176,7 +176,7 @@ <grid id="bcc6c" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <margin top="0" left="0" bottom="0" right="0"/> <constraints> - <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> + <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> <clientProperties> @@ -215,6 +215,15 @@ </component> </children> </grid> + <grid id="3d661" binding="myRightMarginPanel" custom-create="true" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> + <margin top="0" left="0" bottom="0" right="0"/> + <constraints> + <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> + </constraints> + <properties/> + <border type="none"/> + <children/> + </grid> </children> </grid> </children> diff --git a/xml/impl/src/com/intellij/application/options/CodeStyleXmlPanel.java b/xml/impl/src/com/intellij/application/options/CodeStyleXmlPanel.java index ab3126162dde..8767e00de1ec 100644 --- a/xml/impl/src/com/intellij/application/options/CodeStyleXmlPanel.java +++ b/xml/impl/src/com/intellij/application/options/CodeStyleXmlPanel.java @@ -15,6 +15,7 @@ */ package com.intellij.application.options; +import com.intellij.application.options.codeStyle.RightMarginForm; import com.intellij.ide.highlighter.XmlHighlighterFactory; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.highlighter.EditorHighlighter; @@ -25,6 +26,7 @@ import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.intellij.psi.formatter.xml.XmlCodeStyleSettings; import com.intellij.ui.components.JBScrollPane; +import org.apache.xmlbeans.XmlLanguage; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -48,6 +50,8 @@ public class CodeStyleXmlPanel extends CodeStyleAbstractPanel{ private JComboBox myWhiteSpaceAroundCDATA; private JCheckBox myKeepWhitespaceInsideCDATACheckBox; private JBScrollPane myJBScrollPane; + private JPanel myRightMarginPanel; + private RightMarginForm myRightMarginForm; public CodeStyleXmlPanel(CodeStyleSettings settings) { super(settings); @@ -83,6 +87,7 @@ public class CodeStyleXmlPanel extends CodeStyleAbstractPanel{ xmlSettings.XML_SPACE_INSIDE_EMPTY_TAG = myInEmptyTag.isSelected(); xmlSettings.XML_WHITE_SPACE_AROUND_CDATA = myWhiteSpaceAroundCDATA.getSelectedIndex(); xmlSettings.XML_KEEP_WHITE_SPACES_INSIDE_CDATA = myKeepWhitespaceInsideCDATACheckBox.isSelected(); + myRightMarginForm.apply(settings); } private int getIntValue(JTextField keepBlankLines) { @@ -109,6 +114,7 @@ public class CodeStyleXmlPanel extends CodeStyleAbstractPanel{ myWrapText.setSelected(wrapText(settings)); myWhiteSpaceAroundCDATA.setSelectedIndex(xmlSettings.XML_WHITE_SPACE_AROUND_CDATA); myKeepWhitespaceInsideCDATACheckBox.setSelected(xmlSettings.XML_KEEP_WHITE_SPACES_INSIDE_CDATA); + myRightMarginForm.reset(settings); } @Override @@ -156,7 +162,7 @@ public class CodeStyleXmlPanel extends CodeStyleAbstractPanel{ return true; } - return false; + return myRightMarginForm.isModified(settings); } private boolean wrapText(final CodeStyleSettings settings) { @@ -193,5 +199,7 @@ public class CodeStyleXmlPanel extends CodeStyleAbstractPanel{ return new Dimension(prefSize.width + 15, prefSize.height); } }; + myRightMarginForm = new RightMarginForm(StdFileTypes.XML.getLanguage(), getSettings()); + myRightMarginPanel = myRightMarginForm.getTopPanel(); } } diff --git a/xml/impl/src/com/intellij/application/options/HtmlLanguageCodeStyleSettings.java b/xml/impl/src/com/intellij/application/options/HtmlLanguageCodeStyleSettings.java index aa36d1b9aa60..d970ee5008cf 100644 --- a/xml/impl/src/com/intellij/application/options/HtmlLanguageCodeStyleSettings.java +++ b/xml/impl/src/com/intellij/application/options/HtmlLanguageCodeStyleSettings.java @@ -17,10 +17,13 @@ package com.intellij.application.options; import com.intellij.lang.Language; import com.intellij.lang.html.HTMLLanguage; +import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable; import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider; import org.jetbrains.annotations.NotNull; +import java.util.Set; + /** * @author Rustam Vishnyakov */ @@ -37,6 +40,14 @@ public class HtmlLanguageCodeStyleSettings extends LanguageCodeStyleSettingsProv } @Override + public void customizeSettings(@NotNull CodeStyleSettingsCustomizable consumer, + @NotNull SettingsType settingsType) { + if (settingsType == SettingsType.WRAPPING_AND_BRACES_SETTINGS) { + consumer.showStandardOptions("RIGHT_MARGIN"); + } + } + + @Override public CommonCodeStyleSettings getDefaultCommonSettings() { CommonCodeStyleSettings defaultSettings = new CommonCodeStyleSettings(HTMLLanguage.INSTANCE); defaultSettings.initIndentOptions(); diff --git a/xml/impl/src/com/intellij/application/options/XmlLanguageCodeStyleSettingsProvider.java b/xml/impl/src/com/intellij/application/options/XmlLanguageCodeStyleSettingsProvider.java index 651c71e7ee7d..89311331d59b 100644 --- a/xml/impl/src/com/intellij/application/options/XmlLanguageCodeStyleSettingsProvider.java +++ b/xml/impl/src/com/intellij/application/options/XmlLanguageCodeStyleSettingsProvider.java @@ -17,6 +17,7 @@ package com.intellij.application.options; import com.intellij.lang.Language; import com.intellij.lang.xml.XMLLanguage; +import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable; import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider; import com.intellij.util.PlatformUtils; @@ -41,6 +42,14 @@ public class XmlLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSetti } @Override + public void customizeSettings(@NotNull CodeStyleSettingsCustomizable consumer, + @NotNull SettingsType settingsType) { + if (settingsType == SettingsType.WRAPPING_AND_BRACES_SETTINGS) { + consumer.showStandardOptions("RIGHT_MARGIN"); + } + } + + @Override public CommonCodeStyleSettings getDefaultCommonSettings() { CommonCodeStyleSettings xmlSettings = new CommonCodeStyleSettings(getLanguage()); CommonCodeStyleSettings.IndentOptions indentOptions = xmlSettings.initIndentOptions(); diff --git a/xml/impl/src/com/intellij/codeInsight/completion/HtmlTextCompletionConfidence.java b/xml/impl/src/com/intellij/codeInsight/completion/HtmlTextCompletionConfidence.java index ec6c806ffa1f..6d95604dc077 100644 --- a/xml/impl/src/com/intellij/codeInsight/completion/HtmlTextCompletionConfidence.java +++ b/xml/impl/src/com/intellij/codeInsight/completion/HtmlTextCompletionConfidence.java @@ -33,7 +33,9 @@ public class HtmlTextCompletionConfidence extends CompletionConfidence { if (node != null && node.getElementType() == XmlTokenType.XML_DATA_CHARACTERS) { PsiElement parent = contextElement.getParent(); if (parent instanceof XmlText || parent instanceof XmlDocument) { - String prefix = contextElement.getText().substring(0, offset - contextElement.getTextRange().getStartOffset()); + String contextElementText = contextElement.getText(); + int endOffset = offset - contextElement.getTextRange().getStartOffset(); + String prefix = contextElementText.substring(0, Math.min(contextElementText.length(), endOffset)); if (!StringUtil.startsWithChar(prefix, '<') && !StringUtil.startsWithChar(prefix, '&')) { return ThreeState.YES; } diff --git a/xml/impl/src/com/intellij/codeInsight/template/emmet/filters/TrimZenCodingFilter.java b/xml/impl/src/com/intellij/codeInsight/template/emmet/filters/TrimZenCodingFilter.java index ceb0d1dd61b9..421fbdd46564 100644 --- a/xml/impl/src/com/intellij/codeInsight/template/emmet/filters/TrimZenCodingFilter.java +++ b/xml/impl/src/com/intellij/codeInsight/template/emmet/filters/TrimZenCodingFilter.java @@ -18,6 +18,7 @@ package com.intellij.codeInsight.template.emmet.filters; import com.intellij.codeInsight.template.emmet.nodes.GenerationNode; import com.intellij.codeInsight.template.emmet.tokens.TemplateToken; import com.intellij.lang.xml.XMLLanguage; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.psi.PsiElement; import com.intellij.psi.XmlElementVisitor; import com.intellij.psi.xml.XmlDocument; @@ -25,6 +26,7 @@ import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTagValue; import org.jetbrains.annotations.NotNull; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -57,18 +59,27 @@ public class TrimZenCodingFilter extends ZenCodingFilter { if (document != null) { XmlTag tag = document.getRootTag(); if (tag != null && !tag.getText().isEmpty()) { - new XmlElementVisitor() { + tag.accept(new XmlElementVisitor() { @Override - public void visitXmlTag(XmlTag tag) { - if(!tag.isEmpty()) { - XmlTagValue tagValue = tag.getValue(); - tagValue.setText(PATTERN.matcher(tagValue.getText()).replaceAll("")); + public void visitXmlTag(final XmlTag tag) { + if (!tag.isEmpty()) { + final XmlTagValue tagValue = tag.getValue(); + final Matcher matcher = PATTERN.matcher(tagValue.getText()); + if (matcher.matches()) { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + tagValue.setText(matcher.replaceAll("")); + } + }); + } } tag.acceptChildren(this); } - }.visitXmlTag(tag); + }); return tag.getText(); - } else { + } + else { return PATTERN.matcher(document.getText()).replaceAll(""); } } diff --git a/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java b/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java index 86d2994c0e00..4f7f4efa5085 100644 --- a/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java +++ b/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java @@ -18,6 +18,7 @@ package com.intellij.ide.browsers; import com.intellij.concurrency.JobScheduler; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.util.ExecUtil; +import com.intellij.ide.GeneralSettings; import com.intellij.ide.IdeBundle; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.options.ShowSettingsUtil; @@ -25,14 +26,30 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.AppUIUtil; +import com.intellij.util.ArrayUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.net.URI; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; final class BrowserLauncherImpl extends BrowserLauncherAppless { @Override + protected void browseUsingNotSystemDefaultBrowserPolicy(@NotNull URI uri, @NotNull GeneralSettings settings, @Nullable Project project) { + WebBrowserManager browserManager = WebBrowserManager.getInstance(); + if (browserManager.getDefaultBrowserPolicy() == DefaultBrowserPolicy.FIRST) { + WebBrowser browser = browserManager.getFirstActiveBrowser(); + if (browser != null) { + browseUsingPath(uri.toString(), null, browser, project, ArrayUtil.EMPTY_STRING_ARRAY); + return; + } + } + + super.browseUsingNotSystemDefaultBrowserPolicy(uri, settings, project); + } + + @Override protected void doShowError(@Nullable final String error, @Nullable final WebBrowser browser, @Nullable final Project project, final String title, @Nullable final Runnable launchTask) { AppUIUtil.invokeOnEdt(new Runnable() { @Override diff --git a/xml/impl/src/com/intellij/ide/browsers/BrowserSelector.java b/xml/impl/src/com/intellij/ide/browsers/BrowserSelector.java index 3ae67def8424..af04e5896387 100644 --- a/xml/impl/src/com/intellij/ide/browsers/BrowserSelector.java +++ b/xml/impl/src/com/intellij/ide/browsers/BrowserSelector.java @@ -81,8 +81,8 @@ public class BrowserSelector { boolean hasFocus) { Icon baseIcon; if (value == null) { - WebBrowser defaultBrowser = WebBrowserManager.getInstance().getDefaultBrowser(); - baseIcon = defaultBrowser == null ? PlatformIcons.WEB_ICON : defaultBrowser.getIcon(); + WebBrowser firstBrowser = WebBrowserManager.getInstance().getFirstActiveBrowser(); + baseIcon = firstBrowser == null ? PlatformIcons.WEB_ICON : firstBrowser.getIcon(); } else { baseIcon = value.getIcon(); diff --git a/xml/impl/src/com/intellij/ide/browsers/BrowserSettingsPanel.form b/xml/impl/src/com/intellij/ide/browsers/BrowserSettingsPanel.form index 52d074347bbc..23d7bf2ea873 100644 --- a/xml/impl/src/com/intellij/ide/browsers/BrowserSettingsPanel.form +++ b/xml/impl/src/com/intellij/ide/browsers/BrowserSettingsPanel.form @@ -22,7 +22,7 @@ </constraints> <properties/> </component> - <component id="95daa" class="com.intellij.openapi.ui.ComboBox" binding="defaultBrowserComboBox"> + <component id="95daa" class="com.intellij.openapi.ui.ComboBox" binding="defaultBrowserPolicyComboBox"> <constraints> <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="0" indent="0" use-parent-layout="false"/> </constraints> diff --git a/xml/impl/src/com/intellij/ide/browsers/BrowserSettingsPanel.java b/xml/impl/src/com/intellij/ide/browsers/BrowserSettingsPanel.java index 477d527ecbac..f43c0f6f4929 100644 --- a/xml/impl/src/com/intellij/ide/browsers/BrowserSettingsPanel.java +++ b/xml/impl/src/com/intellij/ide/browsers/BrowserSettingsPanel.java @@ -23,7 +23,6 @@ import com.intellij.openapi.options.ShowSettingsUtil; import com.intellij.openapi.ui.ComboBox; import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.util.Comparing; -import com.intellij.ui.EnumComboBoxModel; import com.intellij.ui.ListCellRendererWrapper; import com.intellij.ui.TitledSeparator; import com.intellij.util.Function; @@ -34,6 +33,7 @@ import com.intellij.util.ui.LocalPathCellEditor; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.table.IconTableCellRenderer; import com.intellij.util.ui.table.TableModelEditor; +import org.jdesktop.swingx.combobox.ListComboBoxModel; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -46,9 +46,9 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; +import java.util.ArrayList; import java.util.UUID; -import static com.intellij.ide.browsers.WebBrowserManager.DefaultBrowser; import static com.intellij.util.ui.table.TableModelEditor.EditableColumnInfo; final class BrowserSettingsPanel { @@ -73,7 +73,7 @@ final class BrowserSettingsPanel { } }; - private static final ColumnInfo[] COLUMNS = {new EditableColumnInfo<ConfigurableWebBrowser, Boolean>() { + private static final EditableColumnInfo<ConfigurableWebBrowser, Boolean> ACTIVE_COLUMN_INFO = new EditableColumnInfo<ConfigurableWebBrowser, Boolean>() { @Override public Class getColumnClass() { return Boolean.class; @@ -88,44 +88,49 @@ final class BrowserSettingsPanel { public void setValue(ConfigurableWebBrowser item, Boolean value) { item.setActive(value); } - }, new EditableColumnInfo<ConfigurableWebBrowser, String>("Name") { - @Override - public String valueOf(ConfigurableWebBrowser item) { - return item.getName(); - } + }; - @Override - public void setValue(ConfigurableWebBrowser item, String value) { - item.setName(value); - } - }, new ColumnInfo<ConfigurableWebBrowser, BrowserFamily>("Family") { - @Override - public Class getColumnClass() { - return BrowserFamily.class; - } + private static final ColumnInfo[] COLUMNS = {ACTIVE_COLUMN_INFO, + new EditableColumnInfo<ConfigurableWebBrowser, String>("Name") { + @Override + public String valueOf(ConfigurableWebBrowser item) { + return item.getName(); + } - @Override - public BrowserFamily valueOf(ConfigurableWebBrowser item) { - return item.getFamily(); - } + @Override + public void setValue(ConfigurableWebBrowser item, String value) { + item.setName(value); + } + }, + new ColumnInfo<ConfigurableWebBrowser, BrowserFamily>("Family") { + @Override + public Class getColumnClass() { + return BrowserFamily.class; + } - @Override - public void setValue(ConfigurableWebBrowser item, BrowserFamily value) { - item.setFamily(value); - item.setSpecificSettings(value.createBrowserSpecificSettings()); - } + @Override + public BrowserFamily valueOf(ConfigurableWebBrowser item) { + return item.getFamily(); + } - @Nullable - @Override - public TableCellRenderer getRenderer(ConfigurableWebBrowser item) { - return IconTableCellRenderer.ICONABLE; - } + @Override + public void setValue(ConfigurableWebBrowser item, BrowserFamily value) { + item.setFamily(value); + item.setSpecificSettings(value.createBrowserSpecificSettings()); + } - @Override - public boolean isCellEditable(ConfigurableWebBrowser item) { - return !WebBrowserManager.getInstance().isPredefinedBrowser(item); - } - }, PATH_COLUMN_INFO}; + @Nullable + @Override + public TableCellRenderer getRenderer(ConfigurableWebBrowser item) { + return IconTableCellRenderer.ICONABLE; + } + + @Override + public boolean isCellEditable(ConfigurableWebBrowser item) { + return !WebBrowserManager.getInstance().isPredefinedBrowser(item); + } + }, + PATH_COLUMN_INFO}; private JPanel root; @@ -138,7 +143,7 @@ final class BrowserSettingsPanel { @SuppressWarnings("UnusedDeclaration") private JComponent browsersTable; - private ComboBox defaultBrowserComboBox; + private ComboBox defaultBrowserPolicyComboBox; private TableModelEditor<ConfigurableWebBrowser> browsersEditor; @@ -148,53 +153,55 @@ final class BrowserSettingsPanel { alternativeBrowserPathField.addBrowseFolderListener(IdeBundle.message("title.select.path.to.browser"), null, null, APP_FILE_CHOOSER_DESCRIPTOR); defaultBrowserPanel.setBorder(TitledSeparator.EMPTY_BORDER); - //noinspection unchecked - defaultBrowserComboBox.setModel(new EnumComboBoxModel<DefaultBrowser>(DefaultBrowser.class)); - if (BrowserLauncherAppless.canStartDefaultBrowser()) { - defaultBrowserComboBox.addItemListener(new ItemListener() { - @Override - public void itemStateChanged(ItemEvent e) { - boolean customPathEnabled = e.getItem() == DefaultBrowser.ALTERNATIVE; - if (e.getStateChange() == ItemEvent.DESELECTED) { - if (customPathEnabled) { - customPathValue = alternativeBrowserPathField.getText(); - } - } - else if (e.getStateChange() == ItemEvent.SELECTED) { - alternativeBrowserPathField.setEnabled(customPathEnabled); - updateCustomPathTextFieldValue((DefaultBrowser)e.getItem()); - } - } - }); + ArrayList<DefaultBrowserPolicy> defaultBrowserPolicies = new ArrayList<DefaultBrowserPolicy>(); + if (BrowserLauncherAppless.canUseSystemDefaultBrowserPolicy()) { + defaultBrowserPolicies.add(DefaultBrowserPolicy.SYSTEM); + } + defaultBrowserPolicies.add(DefaultBrowserPolicy.FIRST); + defaultBrowserPolicies.add(DefaultBrowserPolicy.ALTERNATIVE); - defaultBrowserComboBox.setRenderer(new ListCellRendererWrapper<DefaultBrowser>() { - @Override - public void customize(JList list, DefaultBrowser value, int index, boolean selected, boolean hasFocus) { - String name; - switch (value) { - case SYSTEM: - name = "System default"; - break; - case FIRST: - name = "First listed"; - break; - case ALTERNATIVE: - name = "Custom path"; - break; - default: - throw new IllegalStateException(); + //noinspection Since15,unchecked + defaultBrowserPolicyComboBox.setModel(new ListComboBoxModel<DefaultBrowserPolicy>(defaultBrowserPolicies)); + defaultBrowserPolicyComboBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + boolean customPathEnabled = e.getItem() == DefaultBrowserPolicy.ALTERNATIVE; + if (e.getStateChange() == ItemEvent.DESELECTED) { + if (customPathEnabled) { + customPathValue = alternativeBrowserPathField.getText(); } + } + else if (e.getStateChange() == ItemEvent.SELECTED) { + alternativeBrowserPathField.setEnabled(customPathEnabled); + updateCustomPathTextFieldValue((DefaultBrowserPolicy)e.getItem()); + } + } + }); - setText(name); + defaultBrowserPolicyComboBox.setRenderer(new ListCellRendererWrapper<DefaultBrowserPolicy>() { + @Override + public void customize(JList list, DefaultBrowserPolicy value, int index, boolean selected, boolean hasFocus) { + String name; + switch (value) { + case SYSTEM: + name = "System default"; + break; + case FIRST: + name = "First listed"; + break; + case ALTERNATIVE: + name = "Custom path"; + break; + default: + throw new IllegalStateException(); } - }); - if (UIUtil.isUnderAquaLookAndFeel()) { - defaultBrowserComboBox.setBorder(new EmptyBorder(3, 0, 0, 0)); + setText(name); } - } - else { - defaultBrowserComboBox.setVisible(false); + }); + + if (UIUtil.isUnderAquaLookAndFeel()) { + defaultBrowserPolicyComboBox.setBorder(new EmptyBorder(3, 0, 0, 0)); } clearExtractedFiles.addActionListener(new ActionListener() { @@ -205,11 +212,11 @@ final class BrowserSettingsPanel { }); } - private void updateCustomPathTextFieldValue(DefaultBrowser browser) { - if (browser == DefaultBrowser.ALTERNATIVE) { + private void updateCustomPathTextFieldValue(@NotNull DefaultBrowserPolicy browser) { + if (browser == DefaultBrowserPolicy.ALTERNATIVE) { alternativeBrowserPathField.setText(customPathValue); } - else if (browser == DefaultBrowser.FIRST) { + else if (browser == DefaultBrowserPolicy.FIRST) { setCustomPathToFirstListed(); } else { @@ -272,18 +279,18 @@ final class BrowserSettingsPanel { .modelListener(new TableModelEditor.DataChangedListener<ConfigurableWebBrowser>() { @Override public void tableChanged(TableModelEvent event) { - update(event.getFirstRow()); + update(); } @Override public void dataChanged(@NotNull ColumnInfo<ConfigurableWebBrowser, ?> columnInfo, int rowIndex) { - if (columnInfo == PATH_COLUMN_INFO) { - update(rowIndex); + if (columnInfo == PATH_COLUMN_INFO || columnInfo == ACTIVE_COLUMN_INFO) { + update(); } } - private void update(int rowIndex) { - if (rowIndex == 0 && getDefaultBrowser() == DefaultBrowser.FIRST) { + private void update() { + if (getDefaultBrowser() == DefaultBrowserPolicy.FIRST) { setCustomPathToFirstListed(); } } @@ -293,7 +300,15 @@ final class BrowserSettingsPanel { private void setCustomPathToFirstListed() { ListTableModel<ConfigurableWebBrowser> model = browsersEditor.getModel(); - alternativeBrowserPathField.setText(model.getRowCount() == 0 ? "" : model.getRowValue(0).getPath()); + for (int i = 0, n = model.getRowCount(); i < n; i++) { + ConfigurableWebBrowser browser = model.getRowValue(i); + if (browser.isActive() && browser.getPath() != null) { + alternativeBrowserPathField.setText(browser.getPath()); + return; + } + } + + alternativeBrowserPathField.setText(""); } @NotNull @@ -305,12 +320,12 @@ final class BrowserSettingsPanel { WebBrowserManager browserManager = WebBrowserManager.getInstance(); GeneralSettings generalSettings = GeneralSettings.getInstance(); - DefaultBrowser defaultBrowser = getDefaultBrowser(); - if (browserManager.getDefaultBrowserMode() != defaultBrowser || generalSettings.isConfirmExtractFiles() != confirmExtractFiles.isSelected()) { + DefaultBrowserPolicy defaultBrowserPolicy = getDefaultBrowser(); + if (browserManager.getDefaultBrowserPolicy() != defaultBrowserPolicy || generalSettings.isConfirmExtractFiles() != confirmExtractFiles.isSelected()) { return true; } - if (defaultBrowser == DefaultBrowser.ALTERNATIVE && + if (defaultBrowserPolicy == DefaultBrowserPolicy.ALTERNATIVE && !Comparing.strEqual(generalSettings.getBrowserPath(), alternativeBrowserPathField.getText())) { return true; } @@ -321,7 +336,7 @@ final class BrowserSettingsPanel { public void apply() { GeneralSettings settings = GeneralSettings.getInstance(); - settings.setUseDefaultBrowser(getDefaultBrowser() == DefaultBrowser.SYSTEM); + settings.setUseDefaultBrowser(getDefaultBrowser() == DefaultBrowserPolicy.SYSTEM); if (alternativeBrowserPathField.isEnabled()) { settings.setBrowserPath(alternativeBrowserPathField.getText()); @@ -330,26 +345,27 @@ final class BrowserSettingsPanel { settings.setConfirmExtractFiles(confirmExtractFiles.isSelected()); WebBrowserManager browserManager = WebBrowserManager.getInstance(); - browserManager.defaultBrowser = getDefaultBrowser(); + browserManager.defaultBrowserPolicy = getDefaultBrowser(); browserManager.setList(browsersEditor.apply()); } - private DefaultBrowser getDefaultBrowser() { - return (DefaultBrowser)defaultBrowserComboBox.getSelectedItem(); + private DefaultBrowserPolicy getDefaultBrowser() { + return (DefaultBrowserPolicy)defaultBrowserPolicyComboBox.getSelectedItem(); } public void reset() { - GeneralSettings settings = GeneralSettings.getInstance(); - - DefaultBrowser defaultBrowser = WebBrowserManager.getInstance().getDefaultBrowserMode(); - defaultBrowserComboBox.setSelectedItem(defaultBrowser); + DefaultBrowserPolicy defaultBrowserPolicy = WebBrowserManager.getInstance().getDefaultBrowserPolicy(); + DefaultBrowserPolicy effectiveDefaultBrowserPolicy = defaultBrowserPolicy == DefaultBrowserPolicy.SYSTEM && !BrowserLauncherAppless.canUseSystemDefaultBrowserPolicy() + ? DefaultBrowserPolicy.ALTERNATIVE : defaultBrowserPolicy; + defaultBrowserPolicyComboBox.setSelectedItem(effectiveDefaultBrowserPolicy); + GeneralSettings settings = GeneralSettings.getInstance(); confirmExtractFiles.setSelected(settings.isConfirmExtractFiles()); browsersEditor.reset(WebBrowserManager.getInstance().getList()); customPathValue = settings.getBrowserPath(); - alternativeBrowserPathField.setEnabled(defaultBrowser == DefaultBrowser.ALTERNATIVE); - updateCustomPathTextFieldValue(defaultBrowser); + alternativeBrowserPathField.setEnabled(effectiveDefaultBrowserPolicy == DefaultBrowserPolicy.ALTERNATIVE); + updateCustomPathTextFieldValue(effectiveDefaultBrowserPolicy); } public void selectBrowser(@NotNull WebBrowser browser) { diff --git a/xml/impl/src/com/intellij/ide/browsers/ConfigurableWebBrowser.java b/xml/impl/src/com/intellij/ide/browsers/ConfigurableWebBrowser.java index 11502b5119a9..9f48b35905ae 100644 --- a/xml/impl/src/com/intellij/ide/browsers/ConfigurableWebBrowser.java +++ b/xml/impl/src/com/intellij/ide/browsers/ConfigurableWebBrowser.java @@ -56,7 +56,7 @@ final class ConfigurableWebBrowser extends WebBrowser { @Override public Icon getIcon() { if (family == BrowserFamily.CHROME) { - if (checkNameAndPath("Yandex")) { + if (WebBrowserManager.isYandexBrowser(this)) { return AllIcons.Xml.Browsers.Yandex16; } else if (checkNameAndPath("Dartium") || checkNameAndPath("Chromium")) { @@ -76,14 +76,7 @@ final class ConfigurableWebBrowser extends WebBrowser { } private boolean checkNameAndPath(@NotNull String what) { - if (StringUtil.containsIgnoreCase(name, what)) { - return true; - } - if (path != null) { - int index = path.lastIndexOf('/'); - return index > 0 ? path.indexOf(what, index + 1) != -1 : path.contains(what); - } - return false; + return WebBrowserManager.checkNameAndPath(what, this); } @Nullable diff --git a/xml/impl/src/com/intellij/ide/browsers/DefaultBrowserPolicy.java b/xml/impl/src/com/intellij/ide/browsers/DefaultBrowserPolicy.java new file mode 100644 index 000000000000..4e1f0b2e0dc2 --- /dev/null +++ b/xml/impl/src/com/intellij/ide/browsers/DefaultBrowserPolicy.java @@ -0,0 +1,20 @@ +/* + * 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 com.intellij.ide.browsers; + +public enum DefaultBrowserPolicy { + SYSTEM, FIRST, ALTERNATIVE +}
\ No newline at end of file diff --git a/xml/impl/src/com/intellij/ide/browsers/WebBrowserManager.java b/xml/impl/src/com/intellij/ide/browsers/WebBrowserManager.java index ce51ee07eebc..0b2ba6cb18c1 100644 --- a/xml/impl/src/com/intellij/ide/browsers/WebBrowserManager.java +++ b/xml/impl/src/com/intellij/ide/browsers/WebBrowserManager.java @@ -17,10 +17,7 @@ package com.intellij.ide.browsers; import com.intellij.openapi.components.*; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.util.Condition; -import com.intellij.openapi.util.Conditions; -import com.intellij.openapi.util.JDOMUtil; -import com.intellij.openapi.util.SimpleModificationTracker; +import com.intellij.openapi.util.*; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.SmartList; import com.intellij.util.xmlb.SkipDefaultValuesSerializationFilters; @@ -36,53 +33,75 @@ public class WebBrowserManager extends SimpleModificationTracker implements Pers private static final Logger LOG = Logger.getInstance(WebBrowserManager.class); // default standard browser ID must be constant across all IDE versions on all machines for all users - private static final UUID DEFAULT_CHROME_ID = UUID.fromString("98CA6316-2F89-46D9-A9E5-FA9E2B0625B3"); + private static final UUID PREDEFINED_CHROME_ID = UUID.fromString("98CA6316-2F89-46D9-A9E5-FA9E2B0625B3"); // public, but only internal use - public static final UUID DEFAULT_FIREFOX_ID = UUID.fromString("A7BB68E0-33C0-4D6F-A81A-AAC1FDB870C8"); - private static final UUID DEFAULT_SAFARI_ID = UUID.fromString("E5120D43-2C3F-47EF-9F26-65E539E05186"); - private static final UUID DEFAULT_OPERA_ID = UUID.fromString("53E2F627-B1A7-4DFA-BFA7-5B83CC034776"); - private static final UUID DEFAULT_EXPLORER_ID = UUID.fromString("16BF23D4-93E0-4FFC-BFD6-CB13575177B0"); + public static final UUID PREDEFINED_FIREFOX_ID = UUID.fromString("A7BB68E0-33C0-4D6F-A81A-AAC1FDB870C8"); + private static final UUID PREDEFINED_SAFARI_ID = UUID.fromString("E5120D43-2C3F-47EF-9F26-65E539E05186"); + private static final UUID PREDEFINED_OPERA_ID = UUID.fromString("53E2F627-B1A7-4DFA-BFA7-5B83CC034776"); + private static final UUID PREDEFINED_YANDEX_ID = UUID.fromString("B1B2EC2C-20BD-4EE2-89C4-616DB004BCD4"); + private static final UUID PREDEFINED_EXPLORER_ID = UUID.fromString("16BF23D4-93E0-4FFC-BFD6-CB13575177B0"); + + private static final List<ConfigurableWebBrowser> PREDEFINED_BROWSERS = Arrays.asList( + new ConfigurableWebBrowser(PREDEFINED_CHROME_ID, BrowserFamily.CHROME), + new ConfigurableWebBrowser(PREDEFINED_FIREFOX_ID, BrowserFamily.FIREFOX), + new ConfigurableWebBrowser(PREDEFINED_SAFARI_ID, BrowserFamily.SAFARI), + new ConfigurableWebBrowser(PREDEFINED_OPERA_ID, BrowserFamily.OPERA), + new ConfigurableWebBrowser(PREDEFINED_YANDEX_ID, BrowserFamily.CHROME, "Yandex", SystemInfo.isWindows ? "browser" : (SystemInfo.isMac ? "Yandex" : "yandex"), false, BrowserFamily.CHROME.createBrowserSpecificSettings()), + new ConfigurableWebBrowser(PREDEFINED_EXPLORER_ID, BrowserFamily.EXPLORER) + ); private List<ConfigurableWebBrowser> browsers; - DefaultBrowser defaultBrowser = DefaultBrowser.SYSTEM; + DefaultBrowserPolicy defaultBrowserPolicy = DefaultBrowserPolicy.SYSTEM; public WebBrowserManager() { - browsers = new ArrayList<ConfigurableWebBrowser>(); - browsers.add(new ConfigurableWebBrowser(DEFAULT_CHROME_ID, BrowserFamily.CHROME)); - browsers.add(new ConfigurableWebBrowser(DEFAULT_FIREFOX_ID, BrowserFamily.FIREFOX)); - browsers.add(new ConfigurableWebBrowser(DEFAULT_SAFARI_ID, BrowserFamily.SAFARI)); - browsers.add(new ConfigurableWebBrowser(DEFAULT_OPERA_ID, BrowserFamily.OPERA)); - browsers.add(new ConfigurableWebBrowser(DEFAULT_EXPLORER_ID, BrowserFamily.EXPLORER)); + browsers = new ArrayList<ConfigurableWebBrowser>(PREDEFINED_BROWSERS); } public static WebBrowserManager getInstance() { return ServiceManager.getService(WebBrowserManager.class); } - boolean isPredefinedBrowser(@NotNull ConfigurableWebBrowser browser) { - UUID id = browser.getId(); - return id.equals(DEFAULT_CHROME_ID) || - id.equals(DEFAULT_FIREFOX_ID) || - id.equals(DEFAULT_SAFARI_ID) || - id.equals(DEFAULT_OPERA_ID) || - id.equals(DEFAULT_EXPLORER_ID); + public static boolean isYandexBrowser(@NotNull WebBrowser browser) { + return browser.getFamily().equals(BrowserFamily.CHROME) && (browser.getId().equals(PREDEFINED_YANDEX_ID) || checkNameAndPath("Yandex", browser)); + } + + public static boolean isDartium(@NotNull WebBrowser browser) { + return browser.getFamily().equals(BrowserFamily.CHROME) && checkNameAndPath("Dartium", browser); + } + + static boolean checkNameAndPath(@NotNull String what, @NotNull WebBrowser browser) { + if (StringUtil.containsIgnoreCase(browser.getName(), what)) { + return true; + } + String path = browser.getPath(); + if (path != null) { + int index = path.lastIndexOf('/'); + return index > 0 ? path.indexOf(what, index + 1) != -1 : path.contains(what); + } + return false; } - public enum DefaultBrowser { - SYSTEM, FIRST, ALTERNATIVE + boolean isPredefinedBrowser(@NotNull ConfigurableWebBrowser browser) { + UUID id = browser.getId(); + for (ConfigurableWebBrowser predefinedBrowser : PREDEFINED_BROWSERS) { + if (id.equals(predefinedBrowser.getId())) { + return true; + } + } + return false; } @NotNull - public DefaultBrowser getDefaultBrowserMode() { - return defaultBrowser; + public DefaultBrowserPolicy getDefaultBrowserPolicy() { + return defaultBrowserPolicy; } @Override public Element getState() { Element state = new Element("state"); - if (defaultBrowser != DefaultBrowser.SYSTEM) { - state.setAttribute("default", defaultBrowser.name().toLowerCase(Locale.ENGLISH)); + if (defaultBrowserPolicy != DefaultBrowserPolicy.SYSTEM) { + state.setAttribute("default", defaultBrowserPolicy.name().toLowerCase(Locale.ENGLISH)); } for (ConfigurableWebBrowser browser : browsers) { @@ -137,19 +156,19 @@ public class WebBrowserManager extends SimpleModificationTracker implements Pers UUID id; switch (family) { case CHROME: - id = DEFAULT_CHROME_ID; + id = PREDEFINED_CHROME_ID; break; case EXPLORER: - id = DEFAULT_EXPLORER_ID; + id = PREDEFINED_EXPLORER_ID; break; case FIREFOX: - id = DEFAULT_FIREFOX_ID; + id = PREDEFINED_FIREFOX_ID; break; case OPERA: - id = DEFAULT_OPERA_ID; + id = PREDEFINED_OPERA_ID; break; case SAFARI: - id = DEFAULT_SAFARI_ID; + id = PREDEFINED_SAFARI_ID; break; default: @@ -180,7 +199,7 @@ public class WebBrowserManager extends SimpleModificationTracker implements Pers String defaultValue = element.getAttributeValue("default"); if (!StringUtil.isEmpty(defaultValue)) { try { - defaultBrowser = DefaultBrowser.valueOf(defaultValue.toUpperCase(Locale.ENGLISH)); + defaultBrowserPolicy = DefaultBrowserPolicy.valueOf(defaultValue.toUpperCase(Locale.ENGLISH)); } catch (IllegalArgumentException e) { LOG.warn(e); @@ -225,6 +244,18 @@ public class WebBrowserManager extends SimpleModificationTracker implements Pers specificSettings)); } + // add removed/new predefined browsers + int n = list.size(); + pb: for (ConfigurableWebBrowser predefinedBrowser : PREDEFINED_BROWSERS) { + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < n; i++) { + if (list.get(i).getId().equals(predefinedBrowser.getId())) { + continue pb; + } + } + list.add(predefinedBrowser); + } + setList(list); } @@ -349,9 +380,9 @@ public class WebBrowserManager extends SimpleModificationTracker implements Pers } @Nullable - public WebBrowser getDefaultBrowser() { + public WebBrowser getFirstActiveBrowser() { for (ConfigurableWebBrowser browser : browsers) { - if (browser.isActive()) { + if (browser.isActive() && browser.getPath() != null) { return browser; } } diff --git a/xml/impl/src/com/intellij/ide/browsers/actions/BaseOpenInBrowserAction.java b/xml/impl/src/com/intellij/ide/browsers/actions/BaseOpenInBrowserAction.java index 8ec2a2e55b67..e3bf3b5e6f6f 100644 --- a/xml/impl/src/com/intellij/ide/browsers/actions/BaseOpenInBrowserAction.java +++ b/xml/impl/src/com/intellij/ide/browsers/actions/BaseOpenInBrowserAction.java @@ -151,7 +151,7 @@ public abstract class BaseOpenInBrowserAction extends DumbAwareAction { boolean applicable = false; WebBrowserUrlProvider provider = null; if (request != null) { - applicable = HtmlUtil.isHtmlFile(request.getFile()) && !(request.getVirtualFile() instanceof LightVirtualFile); + applicable = WebBrowserServiceImpl.isHtmlOrXmlFile(request.getFile()) && !(request.getVirtualFile() instanceof LightVirtualFile); if (!applicable) { provider = WebBrowserServiceImpl.getProvider(request); applicable = provider != null; diff --git a/xml/impl/src/com/intellij/ide/browsers/actions/OpenFileInDefaultBrowserAction.java b/xml/impl/src/com/intellij/ide/browsers/actions/OpenFileInDefaultBrowserAction.java index 45cf021bbef8..1ead1b6faeb4 100644 --- a/xml/impl/src/com/intellij/ide/browsers/actions/OpenFileInDefaultBrowserAction.java +++ b/xml/impl/src/com/intellij/ide/browsers/actions/OpenFileInDefaultBrowserAction.java @@ -16,10 +16,7 @@ package com.intellij.ide.browsers.actions; import com.intellij.ide.GeneralSettings; -import com.intellij.ide.browsers.OpenInBrowserRequest; -import com.intellij.ide.browsers.WebBrowser; -import com.intellij.ide.browsers.WebBrowserManager; -import com.intellij.ide.browsers.WebBrowserUrlProvider; +import com.intellij.ide.browsers.*; import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.Presentation; @@ -68,10 +65,11 @@ public class OpenFileInDefaultBrowserAction extends DumbAwareAction { @Nullable private static WebBrowser findUsingBrowser() { WebBrowserManager browserManager = WebBrowserManager.getInstance(); - if (browserManager.getDefaultBrowserMode() == WebBrowserManager.DefaultBrowser.FIRST) { - return browserManager.getDefaultBrowser(); + DefaultBrowserPolicy defaultBrowserPolicy = browserManager.getDefaultBrowserPolicy(); + if (defaultBrowserPolicy == DefaultBrowserPolicy.FIRST || (defaultBrowserPolicy == DefaultBrowserPolicy.SYSTEM && !BrowserLauncherAppless.canUseSystemDefaultBrowserPolicy())) { + return browserManager.getFirstActiveBrowser(); } - else if (browserManager.getDefaultBrowserMode() == WebBrowserManager.DefaultBrowser.ALTERNATIVE) { + else if (defaultBrowserPolicy == DefaultBrowserPolicy.ALTERNATIVE) { String path = GeneralSettings.getInstance().getBrowserPath(); if (!StringUtil.isEmpty(path)) { WebBrowser browser = browserManager.findBrowserById(path); diff --git a/xml/impl/src/com/intellij/ide/browsers/impl/WebBrowserServiceImpl.java b/xml/impl/src/com/intellij/ide/browsers/impl/WebBrowserServiceImpl.java index 5cd056f869d9..3d0451d71b3d 100644 --- a/xml/impl/src/com/intellij/ide/browsers/impl/WebBrowserServiceImpl.java +++ b/xml/impl/src/com/intellij/ide/browsers/impl/WebBrowserServiceImpl.java @@ -18,6 +18,10 @@ package com.intellij.ide.browsers.impl; import com.intellij.ide.browsers.OpenInBrowserRequest; import com.intellij.ide.browsers.WebBrowserService; import com.intellij.ide.browsers.WebBrowserUrlProvider; +import com.intellij.lang.Language; +import com.intellij.lang.html.HTMLLanguage; +import com.intellij.lang.xhtml.XHTMLLanguage; +import com.intellij.lang.xml.XMLLanguage; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.impl.http.HttpVirtualFile; @@ -34,6 +38,11 @@ import java.util.Collection; import java.util.Collections; public class WebBrowserServiceImpl extends WebBrowserService { + public static boolean isHtmlOrXmlFile(@NotNull PsiElement element) { + Language language = element.getLanguage(); + return language == HTMLLanguage.INSTANCE || language == XHTMLLanguage.INSTANCE || language == XMLLanguage.INSTANCE; + } + @NotNull @Override public Collection<Url> getUrlsToOpen(@NotNull OpenInBrowserRequest request, boolean preferLocalUrl) throws WebBrowserUrlProvider.BrowserException { @@ -42,7 +51,7 @@ public class WebBrowserServiceImpl extends WebBrowserService { return Collections.singleton(Urls.newFromVirtualFile(virtualFile)); } - if (!preferLocalUrl || !HtmlUtil.isHtmlFile(request.getFile())) { + if (!preferLocalUrl || !isHtmlOrXmlFile(request.getFile())) { WebBrowserUrlProvider provider = getProvider(request); if (provider != null) { if (request.getResult() != null) { diff --git a/xml/impl/src/com/intellij/psi/formatter/xml/AbstractXmlBlock.java b/xml/impl/src/com/intellij/psi/formatter/xml/AbstractXmlBlock.java index 6e1b89912814..b2b5b5ff8eee 100644 --- a/xml/impl/src/com/intellij/psi/formatter/xml/AbstractXmlBlock.java +++ b/xml/impl/src/com/intellij/psi/formatter/xml/AbstractXmlBlock.java @@ -17,6 +17,7 @@ package com.intellij.psi.formatter.xml; import com.intellij.formatting.*; import com.intellij.lang.*; +import com.intellij.lang.xml.XMLLanguage; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileTypes.StdFileTypes; import com.intellij.openapi.util.TextRange; @@ -464,11 +465,28 @@ public abstract class AbstractXmlBlock extends AbstractBlock { return myNode.getElementType() == XmlTokenType.XML_CDATA_END; } - public static boolean containsWhiteSpacesOnly(ASTNode node) { - WhiteSpaceFormattingStrategy strategy = WhiteSpaceFormattingStrategyFactory.getStrategy(node.getPsi().getLanguage()); - String nodeText = node.getText(); - int length = nodeText.length(); - return strategy.check(nodeText, 0, length) >= length; + public static boolean containsWhiteSpacesOnly(@NotNull ASTNode node) { + PsiElement psiElement = node.getPsi(); + if (psiElement instanceof PsiWhiteSpace) return true; + Language nodeLang = psiElement.getLanguage(); + if (!nodeLang.isKindOf(XMLLanguage.INSTANCE) || + isTextOnlyNode(node) || + node.getElementType() == XmlElementType.XML_PROLOG) { + WhiteSpaceFormattingStrategy strategy = WhiteSpaceFormattingStrategyFactory.getStrategy(nodeLang); + int length = node.getTextLength(); + return strategy.check(node.getChars(), 0, length) >= length; + } + return false; + } + + private static boolean isTextOnlyNode(@NotNull ASTNode node) { + if (node.getPsi() instanceof XmlText) return true; + ASTNode firstChild = node.getFirstChildNode(); + ASTNode lastChild = node.getLastChildNode(); + if (firstChild != null && firstChild == lastChild && firstChild.getPsi() instanceof XmlText) { + return true; + } + return false; } } 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 diff --git a/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiChannelHandler.java b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiChannelHandler.java new file mode 100644 index 000000000000..d24078221b30 --- /dev/null +++ b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiChannelHandler.java @@ -0,0 +1,108 @@ +package org.jetbrains.io.fastCgi; + +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.util.text.StringUtilRt; +import com.intellij.util.containers.ConcurrentIntObjectMap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.*; +import org.jetbrains.io.Responses; +import org.jetbrains.io.SimpleChannelInboundHandlerAdapter; + +import static org.jetbrains.io.fastCgi.FastCgiService.LOG; + +@ChannelHandler.Sharable +public class FastCgiChannelHandler extends SimpleChannelInboundHandlerAdapter<FastCgiResponse> { + private final ConcurrentIntObjectMap<Channel> requestToChannel; + + public FastCgiChannelHandler(ConcurrentIntObjectMap<Channel> channel) { + requestToChannel = channel; + } + + @Override + protected void messageReceived(ChannelHandlerContext context, FastCgiResponse response) throws Exception { + ByteBuf buffer = response.getData(); + Channel channel = requestToChannel.remove(response.getId()); + if (channel == null || !channel.isActive()) { + if (buffer != null) { + buffer.release(); + } + return; + } + + if (buffer == null) { + Responses.sendStatus(HttpResponseStatus.BAD_GATEWAY, channel); + return; + } + + HttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer); + try { + parseHeaders(httpResponse, buffer); + Responses.addServer(httpResponse); + if (!HttpHeaders.isContentLengthSet(httpResponse)) { + HttpHeaders.setContentLength(httpResponse, buffer.readableBytes()); + } + } + catch (Throwable e) { + buffer.release(); + Responses.sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, channel); + LOG.error(e); + } + channel.writeAndFlush(httpResponse); + } + + private static void parseHeaders(HttpResponse response, ByteBuf buffer) { + StringBuilder builder = new StringBuilder(); + while (buffer.isReadable()) { + builder.setLength(0); + + String key = null; + boolean valueExpected = true; + while (true) { + int b = buffer.readByte(); + if (b < 0 || b == '\n') { + break; + } + + if (b != '\r') { + if (valueExpected && b == ':') { + valueExpected = false; + + key = builder.toString(); + builder.setLength(0); + skipWhitespace(buffer); + } + else { + builder.append((char)b); + } + } + } + + if (builder.length() == 0) { + // end of headers + return; + } + + // skip standard headers + if (StringUtil.isEmpty(key) || StringUtilRt.startsWithIgnoreCase(key, "http") || StringUtilRt.startsWithIgnoreCase(key, "X-Accel-")) { + continue; + } + + String value = builder.toString(); + if (key.equalsIgnoreCase("status")) { + response.setStatus(HttpResponseStatus.valueOf(Integer.parseInt(value.substring(0, value.indexOf(' '))))); + } + else if (!(key.startsWith("http") || key.startsWith("HTTP"))) { + response.headers().add(key, value); + } + } + } + + private static void skipWhitespace(ByteBuf buffer) { + while (buffer.isReadable() && buffer.getByte(buffer.readerIndex()) == ' ') { + buffer.skipBytes(1); + } + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiConstants.java b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiConstants.java new file mode 100644 index 000000000000..23a85dead1f9 --- /dev/null +++ b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiConstants.java @@ -0,0 +1,5 @@ +package org.jetbrains.io.fastCgi; + +public final class FastCgiConstants { + public static final int HEADER_LENGTH = 8; +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiDecoder.java b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiDecoder.java new file mode 100644 index 000000000000..1cd7adbef5d4 --- /dev/null +++ b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiDecoder.java @@ -0,0 +1,149 @@ +package org.jetbrains.io.fastCgi; + +import com.intellij.util.Consumer; +import gnu.trove.TIntObjectHashMap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.CharsetUtil; +import org.jetbrains.io.Decoder; + +import static org.jetbrains.io.fastCgi.FastCgiService.LOG; + +public class FastCgiDecoder extends Decoder { + private enum State { + HEADER, CONTENT + } + + private State state = State.HEADER; + + private enum ProtocolStatus { + REQUEST_COMPLETE, CANT_MPX_CONN, OVERLOADED, UNKNOWN_ROLE + } + + public static final class RecordType { + public static final int END_REQUEST = 3; + public static final int STDOUT = 6; + public static final int STDERR = 7; + } + + private int type; + private int id; + private int contentLength; + private int paddingLength; + + private final TIntObjectHashMap<ByteBuf> dataBuffers = new TIntObjectHashMap<ByteBuf>(); + + private final Consumer<String> errorOutputConsumer; + + public FastCgiDecoder(Consumer<String> errorOutputConsumer) { + this.errorOutputConsumer = errorOutputConsumer; + } + + @Override + protected void messageReceived(ChannelHandlerContext context, ByteBuf input) throws Exception { + while (true) { + switch (state) { + case HEADER: { + if (paddingLength > 0) { + if (input.readableBytes() >= paddingLength) { + input.skipBytes(paddingLength); + paddingLength = 0; + } + else { + paddingLength -= input.readableBytes(); + input.skipBytes(input.readableBytes()); + input.release(); + return; + } + } + + ByteBuf buffer = getBufferIfSufficient(input, FastCgiConstants.HEADER_LENGTH, context); + if (buffer == null) { + input.release(); + return; + } + + decodeHeader(buffer); + state = State.CONTENT; + } + + case CONTENT: { + if (contentLength > 0) { + ByteBuf buffer = getBufferIfSufficient(input, contentLength, context); + if (buffer == null) { + input.release(); + return; + } + + FastCgiResponse response = readContent(buffer); + if (response != null) { + context.fireChannelRead(response); + } + } + state = State.HEADER; + } + } + } + } + + private void decodeHeader(ByteBuf buffer) { + buffer.skipBytes(1); + type = buffer.readUnsignedByte(); + id = buffer.readUnsignedShort(); + contentLength = buffer.readUnsignedShort(); + paddingLength = buffer.readUnsignedByte(); + buffer.skipBytes(1); + } + + private FastCgiResponse readContent(ByteBuf buffer) { + switch (type) { + case RecordType.END_REQUEST: + int appStatus = buffer.readInt(); + int protocolStatus = buffer.readUnsignedByte(); + buffer.skipBytes(3); + if (appStatus != 0 || protocolStatus != ProtocolStatus.REQUEST_COMPLETE.ordinal()) { + LOG.warn("Protocol status " + protocolStatus); + dataBuffers.remove(id); + return new FastCgiResponse(id, null); + } + else if (protocolStatus == ProtocolStatus.REQUEST_COMPLETE.ordinal()) { + return new FastCgiResponse(id, dataBuffers.remove(id)); + } + break; + + case RecordType.STDOUT: + ByteBuf data = dataBuffers.get(id); + ByteBuf sliced = buffer.slice(buffer.readerIndex(), contentLength); + if (data == null) { + dataBuffers.put(id, sliced); + } + else if (data instanceof CompositeByteBuf) { + ((CompositeByteBuf)data).addComponent(sliced); + data.writerIndex(data.writerIndex() + sliced.readableBytes()); + } + else { + dataBuffers.put(id, Unpooled.wrappedBuffer(data, sliced)); + } + sliced.retain(); + buffer.skipBytes(contentLength); + break; + + case RecordType.STDERR: + try { + errorOutputConsumer.consume(buffer.toString(buffer.readerIndex(), contentLength, CharsetUtil.UTF_8)); + } + catch (Throwable e) { + LOG.error(e); + } + buffer.skipBytes(contentLength); + break; + + default: + LOG.error("Unknown type " + type); + break; + } + return null; + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiRequest.java b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiRequest.java new file mode 100644 index 000000000000..e92d20eebffc --- /dev/null +++ b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiRequest.java @@ -0,0 +1,149 @@ +package org.jetbrains.io.fastCgi; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.util.CharsetUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.builtInWebServer.PathInfo; +import org.jetbrains.builtInWebServer.WebServerPathToFileManager; +import org.jetbrains.io.Responses; + +import java.net.InetSocketAddress; +import java.util.Locale; +import java.util.Map; + +public class FastCgiRequest { + private static final int PARAMS = 4; + private static final int BEGIN_REQUEST = 1; + private static final int RESPONDER = 1; + private static final int FCGI_KEEP_CONNECTION = 1; + private static final int STDIN = 5; + private static final int VERSION = 1; + + private final ByteBuf buffer; + final int requestId; + + public FastCgiRequest(int requestId, @NotNull ByteBufAllocator allocator) { + this.requestId = requestId; + + buffer = allocator.buffer(); + writeHeader(buffer, BEGIN_REQUEST, FastCgiConstants.HEADER_LENGTH); + buffer.writeShort(RESPONDER); + buffer.writeByte(FCGI_KEEP_CONNECTION); + buffer.writeZero(5); + } + + public void writeFileHeaders(@NotNull VirtualFile file, @NotNull Project project, @NotNull CharSequence canonicalRequestPath) { + PathInfo root = WebServerPathToFileManager.getInstance(project).getRoot(file); + FastCgiService.LOG.assertTrue(root != null); + addHeader("DOCUMENT_ROOT", root.getRoot().getPath()); + addHeader("SCRIPT_FILENAME", file.getPath()); + addHeader("SCRIPT_NAME", canonicalRequestPath); + } + + public final void addHeader(@NotNull String key, @Nullable CharSequence value) { + if (value == null) { + return; + } + + int keyLength = key.length(); + int valLength = value.length(); + writeHeader(buffer, PARAMS, keyLength + valLength + (keyLength < 0x80 ? 1 : 4) + (valLength < 0x80 ? 1 : 4)); + + if (keyLength < 0x80) { + buffer.writeByte(keyLength); + } + else { + buffer.writeByte(0x80 | (keyLength >> 24)); + buffer.writeByte(keyLength >> 16); + buffer.writeByte(keyLength >> 8); + buffer.writeByte(keyLength); + } + + if (valLength < 0x80) { + buffer.writeByte(valLength); + } + else { + buffer.writeByte(0x80 | (valLength >> 24)); + buffer.writeByte(valLength >> 16); + buffer.writeByte(valLength >> 8); + buffer.writeByte(valLength); + } + + buffer.writeBytes(key.getBytes(CharsetUtil.US_ASCII)); + buffer.writeBytes(Unpooled.copiedBuffer(value, CharsetUtil.UTF_8)); + } + + public void writeHeaders(FullHttpRequest request, Channel clientChannel) { + addHeader("REQUEST_URI", request.uri()); + addHeader("REQUEST_METHOD", request.method().name()); + + InetSocketAddress remote = (InetSocketAddress)clientChannel.remoteAddress(); + addHeader("REMOTE_ADDR", remote.getAddress().getHostAddress()); + addHeader("REMOTE_PORT", Integer.toString(remote.getPort())); + + InetSocketAddress local = (InetSocketAddress)clientChannel.localAddress(); + addHeader("SERVER_SOFTWARE", Responses.getServerHeaderValue()); + addHeader("SERVER_NAME", Responses.getServerHeaderValue()); + + addHeader("SERVER_ADDR", local.getAddress().getHostAddress()); + addHeader("SERVER_PORT", Integer.toString(local.getPort())); + + addHeader("GATEWAY_INTERFACE", "CGI/1.1"); + addHeader("SERVER_PROTOCOL", request.protocolVersion().text()); + addHeader("CONTENT_TYPE", request.headers().get(HttpHeaders.Names.CONTENT_TYPE)); + + // PHP only, required if PHP was built with --enable-force-cgi-redirect + addHeader("REDIRECT_STATUS", "200"); + + String queryString = ""; + int queryIndex = request.uri().indexOf('?'); + if (queryIndex != -1) { + queryString = request.uri().substring(queryIndex + 1); + } + addHeader("QUERY_STRING", queryString); + + addHeader("CONTENT_LENGTH", String.valueOf(request.content().readableBytes())); + + for (Map.Entry<String, String> entry : request.headers()) { + addHeader("HTTP_" + entry.getKey().replace('-', '_').toUpperCase(Locale.ENGLISH), entry.getValue()); + } + } + + final void writeToServerChannel(ByteBuf content, Channel fastCgiChannel) { + writeHeader(buffer, PARAMS, 0); + fastCgiChannel.write(buffer); + + if (content.isReadable()) { + ByteBuf headerBuffer = fastCgiChannel.alloc().buffer(FastCgiConstants.HEADER_LENGTH, FastCgiConstants.HEADER_LENGTH); + writeHeader(headerBuffer, STDIN, content.readableBytes()); + fastCgiChannel.write(headerBuffer); + + fastCgiChannel.write(content); + + headerBuffer = fastCgiChannel.alloc().buffer(FastCgiConstants.HEADER_LENGTH, FastCgiConstants.HEADER_LENGTH); + writeHeader(headerBuffer, STDIN, 0); + fastCgiChannel.write(headerBuffer); + } + else { + content.release(); + } + + fastCgiChannel.flush(); + } + + private void writeHeader(ByteBuf buffer, int type, int length) { + buffer.writeByte(VERSION); + buffer.writeByte(type); + buffer.writeShort(requestId); + buffer.writeShort(length); + buffer.writeZero(2); + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiResponse.java b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiResponse.java new file mode 100644 index 000000000000..e249f7152c7c --- /dev/null +++ b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiResponse.java @@ -0,0 +1,21 @@ +package org.jetbrains.io.fastCgi; + +import io.netty.buffer.ByteBuf; + +public class FastCgiResponse { + private final int id; + private final ByteBuf data; + + public FastCgiResponse(int id, ByteBuf data) { + this.id = id; + this.data = data; + } + + public ByteBuf getData() { + return data; + } + + public int getId() { + return id; + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiService.java b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiService.java new file mode 100644 index 000000000000..54f13c6c8ea1 --- /dev/null +++ b/xml/impl/src/org/jetbrains/io/fastCgi/FastCgiService.java @@ -0,0 +1,249 @@ +package org.jetbrains.io.fastCgi; + +import com.intellij.concurrency.JobScheduler; +import com.intellij.execution.filters.TextConsoleBuilder; +import com.intellij.execution.filters.TextConsoleBuilderFactory; +import com.intellij.execution.process.OSProcessHandler; +import com.intellij.execution.process.ProcessAdapter; +import com.intellij.execution.process.ProcessEvent; +import com.intellij.execution.ui.ConsoleView; +import com.intellij.execution.ui.ConsoleViewContentType; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.AsyncResult; +import com.intellij.openapi.util.AsyncValueLoader; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowAnchor; +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.ui.content.ContentFactory; +import com.intellij.util.Consumer; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.StripedLockIntObjectConcurrentHashMap; +import com.intellij.util.net.NetUtils; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.handler.codec.http.HttpResponseStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.io.ChannelExceptionHandler; +import org.jetbrains.io.NettyUtil; +import org.jetbrains.io.Responses; + +import javax.swing.*; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +// todo send FCGI_ABORT_REQUEST if client channel disconnected +public abstract class FastCgiService implements Disposable { + static final Logger LOG = Logger.getInstance(FastCgiService.class); + + protected final Project project; + + private final AtomicInteger requestIdCounter = new AtomicInteger(); + private final StripedLockIntObjectConcurrentHashMap<Channel> requests = new StripedLockIntObjectConcurrentHashMap<Channel>(); + + private volatile Channel fastCgiChannel; + + protected final AsyncValueLoader<OSProcessHandler> processHandler = new AsyncValueLoader<OSProcessHandler>() { + @Override + protected boolean isCancelOnReject() { + return true; + } + + @Override + protected void load(@NotNull final AsyncResult<OSProcessHandler> result) throws IOException { + final int port = NetUtils.findAvailableSocketPort(); + final OSProcessHandler processHandler = createProcessHandler(project, port); + if (processHandler == null) { + result.setRejected(); + return; + } + + result.doWhenRejected(new Runnable() { + @Override + public void run() { + processHandler.destroyProcess(); + } + }); + + final MyProcessAdapter processListener = new MyProcessAdapter(); + processHandler.addProcessListener(processListener); + processHandler.startNotify(); + + if (result.isRejected()) { + return; + } + + JobScheduler.getScheduler().schedule(new Runnable() { + @Override + public void run() { + if (result.isRejected()) { + return; + } + + ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { + @Override + public void run() { + if (!result.isRejected()) { + try { + connectToProcess(result, port, processHandler, processListener); + } + catch (Throwable e) { + result.setRejected(); + LOG.error(e); + } + } + } + }); + } + }, NettyUtil.MIN_START_TIME, TimeUnit.MILLISECONDS); + } + + @Override + protected void disposeResult(@NotNull OSProcessHandler processHandler) { + try { + Channel currentFastCgiChannel = fastCgiChannel; + if (currentFastCgiChannel != null) { + fastCgiChannel = null; + NettyUtil.closeAndReleaseFactory(currentFastCgiChannel); + } + processHandler.destroyProcess(); + } + finally { + requestIdCounter.set(0); + if (!requests.isEmpty()) { + List<Channel> waitingClients = ContainerUtil.toList(requests.elements()); + requests.clear(); + for (Channel channel : waitingClients) { + try { + if (channel.isActive()) { + Responses.sendStatus(HttpResponseStatus.BAD_GATEWAY, channel); + } + } + catch (Throwable e) { + NettyUtil.log(e, LOG); + } + } + } + } + } + }; + + private ConsoleView console; + + protected FastCgiService(Project project) { + this.project = project; + } + + protected abstract OSProcessHandler createProcessHandler(Project project, int port); + + private void connectToProcess(final AsyncResult<OSProcessHandler> asyncResult, final int port, final OSProcessHandler processHandler, final Consumer<String> errorOutputConsumer) { + Bootstrap bootstrap = NettyUtil.oioClientBootstrap(); + final FastCgiChannelHandler fastCgiChannelHandler = new FastCgiChannelHandler(requests); + bootstrap.handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel channel) throws Exception { + channel.pipeline().addLast(new FastCgiDecoder(errorOutputConsumer), fastCgiChannelHandler, ChannelExceptionHandler.getInstance()); + } + }); + fastCgiChannel = NettyUtil.connectClient(bootstrap, new InetSocketAddress(NetUtils.getLoopbackAddress(), port), asyncResult); + if (fastCgiChannel != null) { + asyncResult.setDone(processHandler); + } + } + + public void send(final FastCgiRequest fastCgiRequest, final ByteBuf content) { + content.retain(); + + if (processHandler.has()) { + fastCgiRequest.writeToServerChannel(content, fastCgiChannel); + } + else { + processHandler.get().doWhenDone(new Runnable() { + @Override + public void run() { + fastCgiRequest.writeToServerChannel(content, fastCgiChannel); + } + }).doWhenRejected(new Runnable() { + @Override + public void run() { + content.release(); + Channel channel = requests.get(fastCgiRequest.requestId); + if (channel != null && channel.isActive()) { + Responses.sendStatus(HttpResponseStatus.BAD_GATEWAY, channel); + } + } + }); + } + } + + public int allocateRequestId(Channel channel) { + int requestId = requestIdCounter.getAndIncrement(); + if (requestId >= Short.MAX_VALUE) { + requestIdCounter.set(0); + requestId = requestIdCounter.getAndDecrement(); + } + requests.put(requestId, channel); + return requestId; + } + + @Override + public void dispose() { + processHandler.reset(); + } + + protected abstract void buildConsole(@NotNull TextConsoleBuilder consoleBuilder); + + @NotNull + protected abstract String getConsoleToolWindowId(); + + @NotNull + protected abstract Icon getConsoleToolWindowIcon(); + + private final class MyProcessAdapter extends ProcessAdapter implements Consumer<String> { + private void createConsole() { + TextConsoleBuilder consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project); + buildConsole(consoleBuilder); + console = consoleBuilder.getConsole(); + + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + ToolWindow toolWindow = ToolWindowManager.getInstance(project).registerToolWindow(getConsoleToolWindowId(), false, ToolWindowAnchor.BOTTOM, project, true); + toolWindow.setIcon(getConsoleToolWindowIcon()); + toolWindow.getContentManager().addContent(ContentFactory.SERVICE.getInstance().createContent(console.getComponent(), "", false)); + } + }, project.getDisposed()); + } + + @Override + public void onTextAvailable(ProcessEvent event, Key outputType) { + print(event.getText(), ConsoleViewContentType.getConsoleViewType(outputType)); + } + + private void print(String text, ConsoleViewContentType contentType) { + if (console == null) { + createConsole(); + } + console.print(text, contentType); + } + + @Override + public void processTerminated(ProcessEvent event) { + processHandler.reset(); + print(getConsoleToolWindowId() + " terminated\n", ConsoleViewContentType.SYSTEM_OUTPUT); + } + + @Override + public void consume(String message) { + print(message, ConsoleViewContentType.ERROR_OUTPUT); + } + } +}
\ No newline at end of file diff --git a/xml/impl/src/org/jetbrains/notification/SingletonNotificationManager.java b/xml/impl/src/org/jetbrains/notification/SingletonNotificationManager.java new file mode 100644 index 000000000000..4ea4a10dcf85 --- /dev/null +++ b/xml/impl/src/org/jetbrains/notification/SingletonNotificationManager.java @@ -0,0 +1,86 @@ +package org.jetbrains.notification; + +import com.intellij.notification.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindowManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicReference; + +public final class SingletonNotificationManager { + private final AtomicReference<Notification> notification = new AtomicReference<Notification>(); + + private final NotificationGroup group; + private final NotificationType type; + @Nullable + private final NotificationListener listener; + + private Runnable expiredListener; + + public SingletonNotificationManager(@NotNull String groupId, @NotNull NotificationType type, @Nullable NotificationListener listener) { + this(new NotificationGroup(groupId, NotificationDisplayType.STICKY_BALLOON, true), type, listener); + } + + public SingletonNotificationManager(@NotNull NotificationGroup group, @NotNull NotificationType type, @Nullable NotificationListener listener) { + this.group = group; + this.type = type; + this.listener = listener; + } + + public boolean notify(@NotNull String title, @NotNull String content) { + return notify(title, content, null); + } + + public boolean notify(@NotNull String title, @NotNull String content, @Nullable Project project) { + return notify(title, content, listener, project); + } + + public boolean notify(@NotNull String content, @Nullable Project project) { + return notify("", content, listener, project); + } + + public boolean notify(@NotNull String title, + @NotNull String content, + @Nullable NotificationListener listener, + @Nullable Project project) { + Notification oldNotification = notification.get(); + // !oldNotification.isExpired() is not enough - notification could be closed, but not expired + if (oldNotification != null) { + if (!oldNotification.isExpired() && (oldNotification.getBalloon() != null || + (project != null && + group.getDisplayType() == NotificationDisplayType.TOOL_WINDOW && + ToolWindowManager.getInstance(project).getToolWindowBalloon(group.getToolWindowId()) != null))) { + return false; + } + oldNotification.whenExpired(null); + oldNotification.expire(); + } + + if (expiredListener == null) { + expiredListener = new Runnable() { + @Override + public void run() { + Notification currentNotification = notification.get(); + if (currentNotification != null && currentNotification.isExpired()) { + notification.compareAndSet(currentNotification, null); + } + } + }; + } + + Notification newNotification = group.createNotification(title, content, type, listener); + newNotification.whenExpired(expiredListener); + notification.set(newNotification); + newNotification.notify(project); + return true; + } + + public void clear() { + Notification oldNotification = notification.getAndSet(null); + if (oldNotification != null) { + oldNotification.whenExpired(null); + oldNotification.expire(); + } + } +}
\ No newline at end of file diff --git a/xml/impl/xml.iml b/xml/impl/xml.iml index 60e210829091..2fb3ee7ea262 100644 --- a/xml/impl/xml.iml +++ b/xml/impl/xml.iml @@ -21,6 +21,8 @@ <orderEntry type="module" module-name="xml-analysis-impl" exported="" /> <orderEntry type="library" name="swingx" level="project" /> <orderEntry type="module" module-name="xml-structure-view-impl" exported="" /> + <orderEntry type="library" name="Netty" level="project" /> + <orderEntry type="module" module-name="xdebugger-api" /> </component> <component name="copyright"> <Base> |