diff --git a/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/sourceItems/FacetBasedPackagingSourceItemsProvider.java b/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/sourceItems/FacetBasedPackagingSourceItemsProvider.java index c278f5499fe3..b5535a2db956 100644 --- a/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/sourceItems/FacetBasedPackagingSourceItemsProvider.java +++ b/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/sourceItems/FacetBasedPackagingSourceItemsProvider.java @@ -77,7 +77,7 @@ public abstract class FacetBasedPackagingSourceItemsProvider createElement(ArtifactEditorContext context, F facet); - private static class FacetBasedSourceItem extends PackagingSourceItem { + protected static class FacetBasedSourceItem extends PackagingSourceItem { private final FacetBasedPackagingSourceItemsProvider myProvider; private final F myFacet; diff --git a/platform/platform-impl/src/com/intellij/ui/mac/MacFullScreenListener.java b/platform/platform-impl/src/com/intellij/ui/mac/MacFullScreenListener.java new file mode 100644 index 000000000000..d92a4690be18 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/mac/MacFullScreenListener.java @@ -0,0 +1,72 @@ +/* + * Copyright 2000-2012 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.ui.mac; + +import com.apple.eawt.AppEvent; +import com.apple.eawt.FullScreenAdapter; +import com.intellij.Patches; +import com.intellij.openapi.wm.impl.IdeFrameImpl; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class MacFullScreenListener extends FullScreenAdapter { + private final MacMainFrameDecorator myDecorator; + private final IdeFrameImpl myFrame; + + public MacFullScreenListener(final MacMainFrameDecorator decorator, + final IdeFrameImpl frame) { + myDecorator = decorator; + myFrame = frame; + } + + @Override + public void windowEnteredFullScreen(AppEvent.FullScreenEvent event) { + myDecorator.setInFullScreen(true); + + JRootPane rootPane = myFrame.getRootPane(); + if (rootPane != null) rootPane.putClientProperty(MacMainFrameDecorator.FULL_SCREEN, Boolean.TRUE); + if (Patches.APPLE_BUG_ID_10207064) { + // fix problem with bottom empty bar + // it seems like the title is still visible in fullscreen but the window itself shifted up for titlebar height + // and the size of the frame is still calculated to be the height of the screen which is wrong + // so just add these titlebar height to the frame height once again + Timer timer = new Timer(300, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + myFrame.setSize(myFrame.getWidth(), myFrame.getHeight() + myFrame.getInsets().top); + } + }); + } + }); + timer.setRepeats(false); + timer.start(); + } + } + + @Override + public void windowExitedFullScreen(AppEvent.FullScreenEvent event) { + myDecorator.setInFullScreen(false); + myFrame.storeFullScreenStateIfNeeded(false); + + JRootPane rootPane = myFrame.getRootPane(); + if (rootPane != null) rootPane.putClientProperty(MacMainFrameDecorator.FULL_SCREEN, null); + } +} diff --git a/platform/platform-impl/src/com/intellij/ui/mac/MacMainFrameDecorator.java b/platform/platform-impl/src/com/intellij/ui/mac/MacMainFrameDecorator.java index fe295604c47b..46d4b8f1836c 100644 --- a/platform/platform-impl/src/com/intellij/ui/mac/MacMainFrameDecorator.java +++ b/platform/platform-impl/src/com/intellij/ui/mac/MacMainFrameDecorator.java @@ -15,9 +15,6 @@ */ package com.intellij.ui.mac; -import com.apple.eawt.AppEvent; -import com.apple.eawt.FullScreenAdapter; -import com.apple.eawt.FullScreenUtilities; import com.intellij.Patches; import com.intellij.ide.ui.UISettings; import com.intellij.ide.ui.UISettingsListener; @@ -36,8 +33,6 @@ import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.concurrent.atomic.AtomicInteger; @@ -150,43 +145,10 @@ public class MacMainFrameDecorator implements UISettingsListener, Disposable { if (SystemInfo.isMacOSLion) { if (!FULL_SCREEN_AVAILABLE) return; - FullScreenUtilities.addFullScreenListenerTo(frame, new FullScreenAdapter() { - @Override - public void windowEnteredFullScreen(AppEvent.FullScreenEvent event) { - myInFullScreen = true; - - JRootPane rootPane = frame.getRootPane(); - if (rootPane != null) rootPane.putClientProperty(FULL_SCREEN, Boolean.TRUE); - if (Patches.APPLE_BUG_ID_10207064) { - // fix problem with bottom empty bar - // it seems like the title is still visible in fullscreen but the window itself shifted up for titlebar height - // and the size of the frame is still calculated to be the height of the screen which is wrong - // so just add these titlebar height to the frame height once again - Timer timer = new Timer(300, new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - frame.setSize(frame.getWidth(), frame.getHeight() + frame.getInsets().top); - } - }); - } - }); - timer.setRepeats(false); - timer.start(); - } - } - - @Override - public void windowExitedFullScreen(AppEvent.FullScreenEvent event) { - myInFullScreen = false; - frame.storeFullScreenStateIfNeeded(false); - - JRootPane rootPane = frame.getRootPane(); - if (rootPane != null) rootPane.putClientProperty(FULL_SCREEN, null); - } - }); + try { + Class clazz = Class.forName("com.apple.eawt.FullScreenUtilities"); + clazz.getMethod("addFullScreenListenerTo").invoke(null, frame, new MacFullScreenListener(this, frame)); + } catch (Exception ignored) {} } else { // toggle toolbar String className = "IdeaToolbar" + v; @@ -243,4 +205,8 @@ public class MacMainFrameDecorator implements UISettingsListener, Disposable { invoke(window, "toggleFullScreen:", window); } } + + void setInFullScreen(boolean inFullScreen) { + myInFullScreen = inFullScreen; + } } diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/SMTestRunnerConnectionUtil.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/SMTestRunnerConnectionUtil.java index d5990a41826b..99221666a0b4 100644 --- a/platform/smRunner/src/com/intellij/execution/testframework/sm/SMTestRunnerConnectionUtil.java +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/SMTestRunnerConnectionUtil.java @@ -25,9 +25,7 @@ import com.intellij.execution.process.ProcessAdapter; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.testframework.TestConsoleProperties; -import com.intellij.execution.testframework.sm.runner.GeneralToSMTRunnerEventsConvertor; -import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter; -import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties; +import com.intellij.execution.testframework.sm.runner.*; import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView; import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerNotificationsHandler; import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerUIActionsHandler; @@ -87,6 +85,16 @@ public class SMTestRunnerConnectionUtil { final RunnerSettings runnerSettings, final ConfigurationPerRunnerSettings configurationSettings, @Nullable final TestLocationProvider locator) { + return createConsoleWithCustomLocator(testFrameworkName, consoleProperties, runnerSettings, + configurationSettings, locator, false); + } + + public static BaseTestsOutputConsoleView createConsoleWithCustomLocator(@NotNull final String testFrameworkName, + @NotNull final TestConsoleProperties consoleProperties, + final RunnerSettings runnerSettings, + final ConfigurationPerRunnerSettings configurationSettings, + @Nullable final TestLocationProvider locator, + final boolean idBasedTreeConstruction) { // Console final String splitterPropertyName = testFrameworkName + ".Splitter.Proportion"; final SMTRunnerConsoleView console = @@ -97,7 +105,7 @@ public class SMTestRunnerConnectionUtil { super.attachToProcess(processHandler); attachEventsProcessors(consoleProperties, getResultsViewer(), getResultsViewer().getStatisticsPane(), - processHandler, testFrameworkName, locator); + processHandler, testFrameworkName, locator, idBasedTreeConstruction); } }; console.setHelpId("reference.runToolWindow.testResultsTab"); @@ -200,15 +208,24 @@ public class SMTestRunnerConnectionUtil { final StatisticsPanel statisticsPane, final ProcessHandler processHandler, @NotNull final String testFrameworkName, - @Nullable final TestLocationProvider locator) { + @Nullable final TestLocationProvider locator, + boolean idBasedTreeConstruction) { //build messages consumer - final OutputToGeneralTestEventsConverter outputConsumer = consoleProperties instanceof SMCustomMessagesParsing - ? ((SMCustomMessagesParsing)consoleProperties).createTestEventsConverter(testFrameworkName, consoleProperties) - : new OutputToGeneralTestEventsConverter(testFrameworkName, consoleProperties); + final OutputToGeneralTestEventsConverter outputConsumer; + if (consoleProperties instanceof SMCustomMessagesParsing) { + outputConsumer = ((SMCustomMessagesParsing)consoleProperties).createTestEventsConverter(testFrameworkName, consoleProperties); + } + else { + outputConsumer = new OutputToGeneralTestEventsConverter(testFrameworkName, consoleProperties); + } //events processor - final GeneralToSMTRunnerEventsConvertor eventsProcessor = new GeneralToSMTRunnerEventsConvertor(resultsViewer.getTestsRootNode(), - testFrameworkName); + final GeneralTestEventsProcessor eventsProcessor; + if (idBasedTreeConstruction) { + eventsProcessor = new GeneralIdBasedToSMTRunnerEventsConvertor(resultsViewer.getTestsRootNode(), testFrameworkName); + } else { + eventsProcessor = new GeneralToSMTRunnerEventsConvertor(resultsViewer.getTestsRootNode(), testFrameworkName); + } if (locator != null) { eventsProcessor.setLocator(locator); } diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralIdBasedToSMTRunnerEventsConvertor.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralIdBasedToSMTRunnerEventsConvertor.java new file mode 100644 index 000000000000..88ce640ae88f --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralIdBasedToSMTRunnerEventsConvertor.java @@ -0,0 +1,633 @@ +/* + * Copyright 2000-2012 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.execution.testframework.sm.runner; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.intellij.execution.process.ProcessOutputTypes; +import com.intellij.execution.testframework.sm.SMRunnerUtil; +import com.intellij.execution.testframework.sm.SMTestRunnerConnectionUtil; +import com.intellij.execution.testframework.sm.runner.events.*; +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.Key; +import com.intellij.testIntegration.TestLocationProvider; +import gnu.trove.TIntObjectHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * @author Sergey Simonchik + */ +public class GeneralIdBasedToSMTRunnerEventsConvertor implements GeneralTestEventsProcessor { + private static final Logger LOG = Logger.getInstance(GeneralIdBasedToSMTRunnerEventsConvertor.class.getName()); + + private final TIntObjectHashMap myNodeByIdMap = new TIntObjectHashMap(); + private final Set myRunningNodes = Sets.newHashSet(); + private final List myEventsListeners = new ArrayList(); + private final SMTestProxy.SMRootTestProxy myTestsRootProxy; + private final Node myTestsRootNode; + private final String myTestFrameworkName; + private boolean myIsTestingFinished = false; + private TestLocationProvider myLocator = null; + + public GeneralIdBasedToSMTRunnerEventsConvertor(@NotNull SMTestProxy.SMRootTestProxy testsRootProxy, + @NotNull String testFrameworkName) { + myTestsRootProxy = testsRootProxy; + myTestsRootNode = new Node(0, null, testsRootProxy); + myTestFrameworkName = testFrameworkName; + myNodeByIdMap.put(myTestsRootNode.getId(), myTestsRootNode); + myRunningNodes.add(myTestsRootNode); + } + + public void setLocator(@NotNull TestLocationProvider customLocator) { + myLocator = customLocator; + } + + public void addEventsListener(@NotNull SMTRunnerEventsListener listener) { + myEventsListeners.add(listener); + } + + public void onStartTesting() { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + myTestsRootProxy.setStarted(); + + fireOnTestingStarted(); + } + }); + } + + @Override + public void onTestsReporterAttached() { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + myTestsRootProxy.setTestsReporterAttached(); + } + }); + } + + public void onFinishTesting() { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + if (myIsTestingFinished) { + // has been already invoked! + return; + } + myIsTestingFinished = true; + + // We don't know whether process was destroyed by user + // or it finished after all tests have been run + // Lets assume, if at finish all suites except root suite are passed + // then all is ok otherwise process was terminated by user + if (myRunningNodes.size() == 1 && myRunningNodes.contains(myTestsRootNode)) { + myTestsRootProxy.setFinished(); + } else { + logProblem("Unexpected running nodes: " + myRunningNodes); + myTestsRootProxy.setTerminated(); + } + myNodeByIdMap.clear(); + myRunningNodes.clear(); + + fireOnTestingFinished(); + } + }); + } + + public void onTestStarted(@NotNull final TestStartedEvent testStartedEvent) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + doStartNode(testStartedEvent, false); + } + }); + } + + public void onSuiteStarted(@NotNull final TestSuiteStartedEvent suiteStartedEvent) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + doStartNode(suiteStartedEvent, true); + } + }); + } + + private void doStartNode(@NotNull BaseStartedNodeEvent startedNodeEvent, boolean suite) { + Node parentNode = findValidParentNode(startedNodeEvent); + if (parentNode == null) { + return; + } + + if (!validateNodeId(startedNodeEvent)) { + return; + } + int nodeId = startedNodeEvent.getId(); + Node childNode = myNodeByIdMap.get(nodeId); + if (childNode != null) { + logProblem(startedNodeEvent + " has been already started: " + childNode + "!"); + return; + } + + SMTestProxy childProxy = new SMTestProxy(startedNodeEvent.getName(), suite, startedNodeEvent.getLocationUrl(), true); + childNode = new Node(startedNodeEvent.getId(), parentNode, childProxy); + myNodeByIdMap.put(nodeId, childNode); + myRunningNodes.add(childNode); + if (myLocator != null) { + childProxy.setLocator(myLocator); + } + parentNode.getProxy().addChild(childProxy); + + // progress started + childProxy.setStarted(); + if (suite) { + fireOnSuiteStarted(childProxy); + } else { + fireOnTestStarted(childProxy); + } + } + + @Nullable + private Node findValidParentNode(@NotNull BaseStartedNodeEvent startedNodeEvent) { + int parentId = startedNodeEvent.getParentId(); + if (parentId < 0) { + logProblem("Parent node id should be non-negative: " + startedNodeEvent + "."); + return null; + } + Node parentNode = myNodeByIdMap.get(startedNodeEvent.getParentId()); + if (parentNode == null) { + logProblem("Parent node is undefined for " + startedNodeEvent + "."); + return null; + } + if (parentNode.getState() != State.RUNNING) { + logProblem("Parent node should be running: " + parentNode + ", " + startedNodeEvent); + return null; + } + return parentNode; + } + + public void onTestFinished(@NotNull final TestFinishedEvent testFinishedEvent) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + doFinishNode(testFinishedEvent, false); + } + }); + } + + public void onSuiteFinished(@NotNull final TestSuiteFinishedEvent suiteFinishedEvent) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + doFinishNode(suiteFinishedEvent, true); + } + }); + } + + private void doFinishNode(@NotNull TreeNodeEvent treeNodeEvent, boolean suite) { + Node finishedNode = findNode(treeNodeEvent); + if (finishedNode == null) { + String nodeType = suite ? "Suite" : "Test"; + logProblem("Trying to finish not started " + nodeType + ": " + treeNodeEvent); + return; + } + stopRunningNode(finishedNode, State.FINISHED, treeNodeEvent); + finishedNode.getProxy().setFinished(); + if (suite) { + fireOnSuiteFinished(finishedNode.getProxy()); + } else { + fireOnTestFinished(finishedNode.getProxy()); + } + } + + public void onUncapturedOutput(@NotNull final String text, final Key outputType) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + Node activeNode = findActiveNode(); + SMTestProxy activeProxy = activeNode.getProxy(); + if (ProcessOutputTypes.STDERR.equals(outputType)) { + activeProxy.addStdErr(text); + } else if (ProcessOutputTypes.SYSTEM.equals(outputType)) { + activeProxy.addSystemOutput(text); + } else { + activeProxy.addStdOutput(text, outputType); + } + } + }); + } + + public void onError(@NotNull final String localizedMessage, + @Nullable final String stackTrace, + final boolean isCritical) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + Node activeNode = findActiveNode(); + SMTestProxy activeProxy = activeNode.getProxy(); + activeProxy.addError(localizedMessage, stackTrace, isCritical); + } + }); + } + + public void onCustomProgressTestsCategory(@Nullable final String categoryName, + final int testCount) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + fireOnCustomProgressTestsCategory(categoryName, testCount); + } + }); + } + + public void onCustomProgressTestStarted() { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + fireOnCustomProgressTestStarted(); + } + }); + } + + public void onCustomProgressTestFailed() { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + fireOnCustomProgressTestFailed(); + } + }); + } + + public void onTestFailure(@NotNull final TestFailedEvent testFailedEvent) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + Node node = findNode(testFailedEvent); + if (node == null) { + logProblem("Test wasn't started! " + testFailedEvent + "."); + return; + } + stopRunningNode(node, State.FAILED, testFailedEvent); + + SMTestProxy testProxy = node.getProxy(); + + String comparisonFailureActualText = testFailedEvent.getComparisonFailureActualText(); + String comparisonFailureExpectedText = testFailedEvent.getComparisonFailureExpectedText(); + String failureMessage = testFailedEvent.getLocalizedFailureMessage(); + String stackTrace = testFailedEvent.getStacktrace(); + if (comparisonFailureActualText != null && comparisonFailureExpectedText != null) { + testProxy.setTestComparisonFailed(failureMessage, stackTrace, + comparisonFailureActualText, comparisonFailureExpectedText); + } else if (comparisonFailureActualText == null && comparisonFailureExpectedText == null) { + testProxy.setTestFailed(failureMessage, stackTrace, testFailedEvent.isTestError()); + } else { + logProblem("Comparison failure actual and expected texts should be both null or not null.\n" + + "Expected:\n" + + comparisonFailureExpectedText + "\n" + + "Actual:\n" + + comparisonFailureActualText); + } + + // fire event + fireOnTestFailed(testProxy); + } + }); + } + + public void onTestIgnored(@NotNull final TestIgnoredEvent testIgnoredEvent) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + Node node = findNode(testIgnoredEvent); + if (node == null) { + logProblem("Test wasn't started! " + testIgnoredEvent + "."); + return; + } + stopRunningNode(node, State.IGNORED, testIgnoredEvent); + + SMTestProxy testProxy = node.getProxy(); + testProxy.setTestIgnored(testIgnoredEvent.getIgnoreComment(), testIgnoredEvent.getStacktrace()); + + // fire event + fireOnTestIgnored(testProxy); + } + }); + } + + public void onTestOutput(@NotNull final TestOutputEvent testOutputEvent) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + Node node = findNode(testOutputEvent); + if (node == null) { + logProblem("Test wasn't started! But " + testOutputEvent + "!"); + return; + } + SMTestProxy testProxy = node.getProxy(); + + if (testOutputEvent.isStdOut()) { + testProxy.addStdOutput(testOutputEvent.getText(), ProcessOutputTypes.STDOUT); + } else { + testProxy.addStdErr(testOutputEvent.getText()); + } + } + }); + } + + public void onTestsCountInSuite(final int count) { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + fireOnTestsCountInSuite(count); + } + }); + } + + private boolean validateNodeId(@NotNull TreeNodeEvent treeNodeEvent) { + int nodeId = treeNodeEvent.getId(); + if (nodeId <= 0) { + logProblem("Node id should be positive: " + treeNodeEvent + "."); + return false; + } + return true; + } + + @Nullable + private Node findNode(@NotNull TreeNodeEvent treeNodeEvent) { + if (!validateNodeId(treeNodeEvent)) { + return null; + } + return myNodeByIdMap.get(treeNodeEvent.getId()); + } + + private void fireOnTestingStarted() { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onTestingStarted(myTestsRootProxy); + } + } + + private void fireOnTestingFinished() { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onTestingFinished(myTestsRootProxy); + } + } + + private void fireOnTestsCountInSuite(final int count) { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onTestsCountInSuite(count); + } + } + + + private void fireOnTestStarted(final SMTestProxy test) { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onTestStarted(test); + } + } + + private void fireOnTestFinished(final SMTestProxy test) { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onTestFinished(test); + } + } + + private void fireOnTestFailed(final SMTestProxy test) { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onTestFailed(test); + } + } + + private void fireOnTestIgnored(final SMTestProxy test) { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onTestIgnored(test); + } + } + + private void fireOnSuiteStarted(final SMTestProxy suite) { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onSuiteStarted(suite); + } + } + + private void fireOnSuiteFinished(final SMTestProxy suite) { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onSuiteFinished(suite); + } + } + + + private void fireOnCustomProgressTestsCategory(@Nullable final String categoryName, int testCount) { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onCustomProgressTestsCategory(categoryName, testCount); + } + } + + private void fireOnCustomProgressTestStarted() { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onCustomProgressTestStarted(); + } + } + + private void fireOnCustomProgressTestFailed() { + for (SMTRunnerEventsListener listener : myEventsListeners) { + listener.onCustomProgressTestFailed(); + } + } + + /* + * Remove listeners, etc + */ + public void dispose() { + SMRunnerUtil.addToInvokeLater(new Runnable() { + public void run() { + myEventsListeners.clear(); + + if (!myRunningNodes.isEmpty()) { + Application application = ApplicationManager.getApplication(); + if (!application.isHeadlessEnvironment() && !application.isUnitTestMode()) { + logProblem("Not all events were processed!"); + } + } + myRunningNodes.clear(); + myNodeByIdMap.clear(); + } + }); + } + + private void stopRunningNode(@NotNull Node node, @NotNull State stoppedState, @NotNull TreeNodeEvent event) { + if (stoppedState == State.RUNNING) { + throw new RuntimeException("newState shouldn't be " + State.RUNNING); + } + // check if has been already processed + if (node.getState() != State.RUNNING) { + logProblem("Can't change state of already stopped node" + node + " to " + stoppedState + ", " + event + "."); + return; + } + myRunningNodes.remove(node); + node.setState(stoppedState); + } + + @NotNull + private Node findActiveNode() { + List runningLeaves = Lists.newArrayListWithExpectedSize(1); + for (Node node : myRunningNodes) { + if (!node.hasRunningChildren()) { + runningLeaves.add(node); + } + } + if (runningLeaves.isEmpty()) { + throw new RuntimeException("No running leaves found, running nodes: " + myRunningNodes); + } + if (runningLeaves.size() == 1) { + return runningLeaves.iterator().next(); + } + List commonPathToRoot = null; + for (Node leaf : runningLeaves) { + List pathToRoot = leaf.getAncestorsFromParentToRoot(); + if (commonPathToRoot == null) { + commonPathToRoot = pathToRoot; + } else { + commonPathToRoot = intersectPathsToRoot(commonPathToRoot, pathToRoot); + } + } + if (commonPathToRoot == null || commonPathToRoot.isEmpty()) { + throw new RuntimeException("Unexpected common path to root: " + commonPathToRoot + ", running leaves: " + runningLeaves); + } + return commonPathToRoot.get(0); + } + + @NotNull + private static List intersectPathsToRoot(@NotNull List pathToRoot1, @NotNull List pathToRoot2) { + final int minSize = Math.min(pathToRoot1.size(), pathToRoot2.size()); + final int shift1 = pathToRoot1.size() - minSize; + final int shift2 = pathToRoot2.size() - minSize; + int commonSize = 0; + for (int i = 0; i < minSize; i++) { + Node node1 = pathToRoot1.get(i + shift1); + Node node2 = pathToRoot2.get(i + shift2); + if (node1 == node2) { + commonSize = minSize - i; + break; + } + } + return pathToRoot1.subList(pathToRoot1.size() - commonSize, pathToRoot1.size()); + } + + private static String getTestFrameworkPrefix(@NotNull String testFrameworkName) { + return "[" + testFrameworkName + "] "; + } + + private void logProblem(@NotNull String msg) { + logProblem(LOG, msg, myTestFrameworkName); + } + + private static void logProblem(@NotNull Logger log, @NotNull String msg, @NotNull String testFrameworkName) { + logProblem(log, msg, SMTestRunnerConnectionUtil.isInDebugMode(), testFrameworkName); + } + + private static void logProblem(@NotNull Logger log, @NotNull String msg, boolean throwError, @NotNull String testFrameworkName) { + final String text = getTestFrameworkPrefix(testFrameworkName) + msg; + if (throwError) { + log.error(text); + } + else { + log.warn(text); + } + } + + private enum State { + RUNNING, FINISHED, FAILED, IGNORED + } + + private static class Node { + private final int myId; + private final Node myParentNode; + private final SMTestProxy myProxy; + private State myState = State.RUNNING; + private int myRunningChildCount = 0; + + Node(int id, @Nullable Node parentNode, @NotNull SMTestProxy proxy) { + myId = id; + myParentNode = parentNode; + myProxy = proxy; + if (myParentNode != null) { + myParentNode.myRunningChildCount++; + } + } + + public int getId() { + return myId; + } + + @Nullable + public Node getParentNode() { + return myParentNode; + } + + @NotNull + public SMTestProxy getProxy() { + return myProxy; + } + + @NotNull + public State getState() { + return myState; + } + + public void setState(@NotNull State state) { + if (myState == State.RUNNING && state != State.RUNNING) { + if (myParentNode != null) { + myParentNode.myRunningChildCount--; + } + } else { + throw new RuntimeException("Attempt to change state from " + myState + " to " + state + ":" + toString()); + } + myState = state; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Node node = (Node)o; + + return myId == node.myId; + } + + @Override + public int hashCode() { + return myId; + } + + @Override + public String toString() { + return "{" + + "id=" + myId + + ", parentId=" + (myParentNode != null ? String.valueOf(myParentNode.getId()) : "") + + ", name='" + myProxy.getName() + + "', isSuite=" + myProxy.isSuite() + + ", state=" + myState + + '}'; + } + + public boolean hasRunningChildren() { + return myRunningChildCount > 0; + } + + @NotNull + public List getAncestorsFromParentToRoot() { + List ancestors = Lists.newArrayList(); + Node parent = getParentNode(); + while (parent != null) { + ancestors.add(parent); + parent = parent.getParentNode(); + } + return ancestors; + } + } +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralTestEventsProcessor.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralTestEventsProcessor.java index ffa8f8bd185e..62f9564789df 100644 --- a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralTestEventsProcessor.java +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralTestEventsProcessor.java @@ -15,8 +15,10 @@ */ package com.intellij.execution.testframework.sm.runner; +import com.intellij.execution.testframework.sm.runner.events.*; import com.intellij.openapi.Disposable; import com.intellij.openapi.util.Key; +import com.intellij.testIntegration.TestLocationProvider; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,33 +31,23 @@ import org.jetbrains.annotations.Nullable; * and name of test method */ public interface GeneralTestEventsProcessor extends Disposable { + void onStartTesting(); + void onTestsCountInSuite(final int count); - void onTestStarted(@NotNull final String testName, - @Nullable final String locationUrl); + void onTestStarted(@NotNull TestStartedEvent testStartedEvent); - void onTestFinished(@NotNull final String testName, - final int duration); + void onTestFinished(@NotNull TestFinishedEvent testFinishedEvent); - void onTestFailure(@NotNull final String testName, - @NotNull final String localizedMessage, - @Nullable final String stackTrace, - final boolean testError, - @Nullable final String comparisionFailureActualText, - @Nullable final String comparisionFailureExpectedText); + void onTestFailure(@NotNull TestFailedEvent testFailedEvent); - void onTestIgnored(@NotNull final String testName, - @NotNull final String ignoreComment, - @Nullable final String stackTrace); + void onTestIgnored(@NotNull TestIgnoredEvent testIgnoredEvent); - void onTestOutput(@NotNull final String testName, - @NotNull final String text, - final boolean stdOut); + void onTestOutput(@NotNull TestOutputEvent testOutputEvent); - void onSuiteStarted(@NotNull final String suiteName, - @Nullable final String locationUrl); + void onSuiteStarted(@NotNull TestSuiteStartedEvent suiteStartedEvent); - void onSuiteFinished(@NotNull final String suiteName); + void onSuiteFinished(@NotNull TestSuiteFinishedEvent suiteFinishedEvent); void onUncapturedOutput(@NotNull final String text, final Key outputType); @@ -76,4 +68,10 @@ public interface GeneralTestEventsProcessor extends Disposable { void onCustomProgressTestStarted(); void onCustomProgressTestFailed(); void onTestsReporterAttached(); -} \ No newline at end of file + + void setLocator(@NotNull TestLocationProvider locator); + + void addEventsListener(@NotNull SMTRunnerEventsListener viewer); + + void onFinishTesting(); +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralToSMTRunnerEventsConvertor.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralToSMTRunnerEventsConvertor.java index d7fe14011a7f..2e38c07d0594 100644 --- a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralToSMTRunnerEventsConvertor.java +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/GeneralToSMTRunnerEventsConvertor.java @@ -19,11 +19,13 @@ import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.execution.testframework.AbstractTestProxy; import com.intellij.execution.testframework.sm.SMRunnerUtil; import com.intellij.execution.testframework.sm.SMTestRunnerConnectionUtil; +import com.intellij.execution.testframework.sm.runner.events.*; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Key; import com.intellij.testIntegration.TestLocationProvider; +import com.intellij.util.ObjectUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -55,11 +57,11 @@ public class GeneralToSMTRunnerEventsConvertor implements GeneralTestEventsProce myTestFrameworkName = testFrameworkName; } - public void setLocator(TestLocationProvider customLocator) { + public void setLocator(@NotNull TestLocationProvider customLocator) { myLocator = customLocator; } - public void addEventsListener(final SMTRunnerEventsListener listener) { + public void addEventsListener(@NotNull final SMTRunnerEventsListener listener) { myEventsListeners.add(listener); } @@ -111,10 +113,11 @@ public class GeneralToSMTRunnerEventsConvertor implements GeneralTestEventsProce }); } - public void onTestStarted(@NotNull final String testName, - @Nullable final String locationUrl) { + public void onTestStarted(@NotNull final TestStartedEvent testStartedEvent) { SMRunnerUtil.addToInvokeLater(new Runnable() { public void run() { + final String testName = testStartedEvent.getName(); + final String locationUrl = testStartedEvent.getLocationUrl(); final String fullName = getFullTestName(testName); if (myRunningTestsFullNameToProxy.containsKey(fullName)) { @@ -146,9 +149,11 @@ public class GeneralToSMTRunnerEventsConvertor implements GeneralTestEventsProce }); } - public void onSuiteStarted(@NotNull final String suiteName, @Nullable final String locationUrl) { + public void onSuiteStarted(@NotNull final TestSuiteStartedEvent suiteStartedEvent) { SMRunnerUtil.addToInvokeLater(new Runnable() { public void run() { + final String suiteName = suiteStartedEvent.getName(); + final String locationUrl = suiteStartedEvent.getLocationUrl(); final SMTestProxy parentSuite = getCurrentSuite(); //new suite final SMTestProxy newSuite = new SMTestProxy(suiteName, true, locationUrl); @@ -168,10 +173,11 @@ public class GeneralToSMTRunnerEventsConvertor implements GeneralTestEventsProce }); } - public void onTestFinished(@NotNull final String testName, - final int duration) { + public void onTestFinished(@NotNull final TestFinishedEvent testFinishedEvent) { SMRunnerUtil.addToInvokeLater(new Runnable() { public void run() { + final String testName = testFinishedEvent.getName(); + final int duration = testFinishedEvent.getDuration(); final String fullTestName = getFullTestName(testName); final SMTestProxy testProxy = getProxyByFullTestName(fullTestName); @@ -191,9 +197,10 @@ public class GeneralToSMTRunnerEventsConvertor implements GeneralTestEventsProce }); } - public void onSuiteFinished(@NotNull final String suiteName) { + public void onSuiteFinished(@NotNull final TestSuiteFinishedEvent suiteFinishedEvent) { SMRunnerUtil.addToInvokeLater(new Runnable() { public void run() { + final String suiteName = suiteFinishedEvent.getName(); final SMTestProxy mySuite = mySuitesStack.popSuite(suiteName); if (mySuite != null) { mySuite.setFinished(); @@ -257,14 +264,15 @@ public class GeneralToSMTRunnerEventsConvertor implements GeneralTestEventsProce }); } - public void onTestFailure(@NotNull final String testName, - @NotNull final String localizedMessage, - @Nullable final String stackTrace, - final boolean isTestError, - @Nullable final String comparisionFailureActualText, - @Nullable final String comparisionFailureExpectedText) { + public void onTestFailure(@NotNull final TestFailedEvent testFailedEvent) { SMRunnerUtil.addToInvokeLater(new Runnable() { public void run() { + final String testName = ObjectUtils.assertNotNull(testFailedEvent.getName()); + final String localizedMessage = testFailedEvent.getLocalizedFailureMessage(); + final String stackTrace = testFailedEvent.getStacktrace(); + final boolean isTestError = testFailedEvent.isTestError(); + final String comparisionFailureActualText = testFailedEvent.getComparisonFailureActualText(); + final String comparisionFailureExpectedText = testFailedEvent.getComparisonFailureExpectedText(); final boolean inDebugMode = SMTestRunnerConnectionUtil.isInDebugMode(); final String fullTestName = getFullTestName(testName); @@ -281,7 +289,7 @@ public class GeneralToSMTRunnerEventsConvertor implements GeneralTestEventsProce if (!myFailedTestsSet.contains(testProxy)) { // if hasn't been already reported // 1. report - onTestStarted(testName, null); + onTestStarted(new TestStartedEvent(testName, null)); // 2. add failure testProxy = getProxyByFullTestName(fullTestName); } @@ -323,11 +331,12 @@ public class GeneralToSMTRunnerEventsConvertor implements GeneralTestEventsProce }); } - public void onTestIgnored(@NotNull final String testName, - @NotNull final String ignoreComment, - @Nullable final String stackTrace) { + public void onTestIgnored(@NotNull final TestIgnoredEvent testIgnoredEvent) { SMRunnerUtil.addToInvokeLater(new Runnable() { public void run() { + final String testName = ObjectUtils.assertNotNull(testIgnoredEvent.getName()); + final String ignoreComment = testIgnoredEvent.getIgnoreComment(); + final String stackTrace = testIgnoredEvent.getStacktrace(); final String fullTestName = getFullTestName(testName); SMTestProxy testProxy = getProxyByFullTestName(fullTestName); if (testProxy == null) { @@ -341,7 +350,7 @@ public class GeneralToSMTRunnerEventsConvertor implements GeneralTestEventsProce } else { // try to fix // 1. report test opened - onTestStarted(testName, null); + onTestStarted(new TestStartedEvent(testName, null)); // 2. report failure testProxy = getProxyByFullTestName(fullTestName); @@ -359,10 +368,12 @@ public class GeneralToSMTRunnerEventsConvertor implements GeneralTestEventsProce }); } - public void onTestOutput(@NotNull final String testName, - @NotNull final String text, final boolean stdOut) { + public void onTestOutput(@NotNull final TestOutputEvent testOutputEvent) { SMRunnerUtil.addToInvokeLater(new Runnable() { public void run() { + final String testName = testOutputEvent.getName(); + final String text = testOutputEvent.getText(); + final boolean stdOut = testOutputEvent.isStdOut(); final String fullTestName = getFullTestName(testName); final SMTestProxy testProxy = getProxyByFullTestName(fullTestName); if (testProxy == null) { diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputToGeneralTestEventsConverter.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputToGeneralTestEventsConverter.java index c57111d9db64..9554844e1455 100644 --- a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputToGeneralTestEventsConverter.java +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputToGeneralTestEventsConverter.java @@ -17,6 +17,7 @@ package com.intellij.execution.testframework.sm.runner; import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.execution.testframework.TestConsoleProperties; +import com.intellij.execution.testframework.sm.runner.events.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.text.StringUtil; @@ -41,13 +42,11 @@ import static com.intellij.execution.testframework.sm.runner.GeneralToSMTRunnerE public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer { private static final Logger LOG = Logger.getInstance(OutputToGeneralTestEventsConverter.class.getName()); - private static final String TEAMCITY_SERVICE_MESSAGE_PREFIX = "##teamcity["; - private GeneralTestEventsProcessor myProcessor; private final MyServiceMessageVisitor myServiceMessageVisitor; private final String myTestFrameworkName; - private OutputLineSplitter mySplitter; + private final OutputLineSplitter mySplitter; private boolean myPendingLineBreakFlag; public OutputToGeneralTestEventsConverter(@NotNull final String testFrameworkName, @@ -63,7 +62,7 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer }; } - public void setProcessor(final GeneralTestEventsProcessor processor) { + public void setProcessor(@Nullable final GeneralTestEventsProcessor processor) { myProcessor = processor; } @@ -142,52 +141,38 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer } - private void fireOnTestStarted(final String testName, @Nullable final String locationUrl) { - assertNotNull(testName); - + private void fireOnTestStarted(@NotNull TestStartedEvent testStartedEvent) { // local variable is used to prevent concurrent modification final GeneralTestEventsProcessor processor = myProcessor; if (processor != null) { - processor.onTestStarted(testName, locationUrl); + processor.onTestStarted(testStartedEvent); } } - private void fireOnTestFailure(final String testName, - final String localizedMessage, final String stackTrace, - final boolean isTestError, - @Nullable final String comparisionFailureActualText, - @Nullable final String comparisionFailureExpectedText) { - assertNotNull(testName); - assertNotNull(localizedMessage); + private void fireOnTestFailure(@NotNull TestFailedEvent testFailedEvent) { + assertNotNull(testFailedEvent.getLocalizedFailureMessage()); // local variable is used to prevent concurrent modification final GeneralTestEventsProcessor processor = myProcessor; if (processor != null) { - processor.onTestFailure(testName, localizedMessage, stackTrace, isTestError, - comparisionFailureActualText, - comparisionFailureExpectedText); + processor.onTestFailure(testFailedEvent); } } - private void fireOnTestIgnored(final String testName, final String ignoreComment, - @Nullable final String details) { - assertNotNull(testName); - assertNotNull(ignoreComment); + private void fireOnTestIgnored(@NotNull TestIgnoredEvent testIgnoredEvent) { // local variable is used to prevent concurrent modification final GeneralTestEventsProcessor processor = myProcessor; if (processor != null) { - processor.onTestIgnored(testName, ignoreComment, details); + processor.onTestIgnored(testIgnoredEvent); } } - private void fireOnTestFinished(final String testName, final int duration) { - assertNotNull(testName); - + private void fireOnTestFinished(@NotNull TestFinishedEvent testFinishedEvent) { // local variable is used to prevent concurrent modification final GeneralTestEventsProcessor processor = myProcessor; if (processor != null) { - processor.onTestFinished(testName, duration); + processor.onTestFinished(testFinishedEvent); } } @@ -224,14 +209,11 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer } } - private void fireOnTestOutput(final String testName, final String text, final boolean stdOut) { - assertNotNull(testName); - assertNotNull(text); - + private void fireOnTestOutput(@NotNull TestOutputEvent testOutputEvent) { // local variable is used to prevent concurrent modification final GeneralTestEventsProcessor processor = myProcessor; if (processor != null) { - processor.onTestOutput(testName, text, stdOut); + processor.onTestOutput(testOutputEvent); } } @@ -257,23 +239,19 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer } } - private void fireOnSuiteStarted(final String suiteName, @Nullable final String locationUrl) { - assertNotNull(suiteName); - + private void fireOnSuiteStarted(@NotNull TestSuiteStartedEvent suiteStartedEvent) { // local variable is used to prevent concurrent modification final GeneralTestEventsProcessor processor = myProcessor; if (processor != null) { - processor.onSuiteStarted(suiteName, locationUrl); + processor.onSuiteStarted(suiteStartedEvent); } } - private void fireOnSuiteFinished(final String suiteName) { - assertNotNull(suiteName); - + private void fireOnSuiteFinished(@NotNull TestSuiteFinishedEvent nodeFinishedEvent) { // local variable is used to prevent concurrent modification final GeneralTestEventsProcessor processor = myProcessor; if (processor != null) { - processor.onSuiteFinished(suiteName); + processor.onSuiteFinished(nodeFinishedEvent); } } @@ -321,7 +299,8 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer public void visitTestSuiteStarted(@NotNull final TestSuiteStarted suiteStarted) { final String locationUrl = fetchTestLocation(suiteStarted); - fireOnSuiteStarted(suiteStarted.getSuiteName(), locationUrl); + TestSuiteStartedEvent suiteStartedEvent = new TestSuiteStartedEvent(suiteStarted, locationUrl); + fireOnSuiteStarted(suiteStartedEvent); } @Nullable @@ -343,7 +322,8 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer } public void visitTestSuiteFinished(@NotNull final TestSuiteFinished suiteFinished) { - fireOnSuiteFinished(suiteFinished.getSuiteName()); + TestSuiteFinishedEvent finishedEvent = new TestSuiteFinishedEvent(suiteFinished); + fireOnSuiteFinished(finishedEvent); } public void visitTestStarted(@NotNull final TestStarted testStarted) { @@ -351,7 +331,8 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer // final String locationUrl = testStarted.getLocationHint(); final String locationUrl = testStarted.getAttributes().get(ATTR_KEY_LOCATION_URL); - fireOnTestStarted(testStarted.getTestName(), locationUrl); + TestStartedEvent testStartedEvent = new TestStartedEvent(testStarted, locationUrl); + fireOnTestStarted(testStartedEvent); } public void visitTestFinished(@NotNull final TestFinished testFinished) { @@ -368,31 +349,27 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer duration = convertToInt(durationStr, testFinished); } - fireOnTestFinished(testFinished.getTestName(), duration); + TestFinishedEvent testFinishedEvent = new TestFinishedEvent(testFinished, duration); + fireOnTestFinished(testFinishedEvent); } public void visitTestIgnored(@NotNull final TestIgnored testIgnored) { - final String details = testIgnored.getAttributes().get(ATTR_KEY_STACKTRACE_DETAILS); - fireOnTestIgnored(testIgnored.getTestName(), testIgnored.getIgnoreComment(), details); + final String stacktrace = testIgnored.getAttributes().get(ATTR_KEY_STACKTRACE_DETAILS); + fireOnTestIgnored(new TestIgnoredEvent(testIgnored, stacktrace)); } public void visitTestStdOut(@NotNull final TestStdOut testStdOut) { - fireOnTestOutput(testStdOut.getTestName(), testStdOut.getStdOut(), true); + fireOnTestOutput(new TestOutputEvent(testStdOut, testStdOut.getStdOut(), true)); } public void visitTestStdErr(@NotNull final TestStdErr testStdErr) { - fireOnTestOutput(testStdErr.getTestName(), testStdErr.getStdErr(), false); + fireOnTestOutput(new TestOutputEvent(testStdErr.getTestName(), testStdErr.getStdErr(), false)); } public void visitTestFailed(@NotNull final TestFailed testFailed) { - final boolean isTestError = testFailed.getAttributes().get(ATTR_KEY_TEST_ERROR) != null; - - fireOnTestFailure(testFailed.getTestName(), - testFailed.getFailureMessage(), - testFailed.getStacktrace(), - isTestError, - testFailed.getActual(), - testFailed.getExpected()); + final boolean testError = testFailed.getAttributes().get(ATTR_KEY_TEST_ERROR) != null; + TestFailedEvent testFailedEvent = new TestFailedEvent(testFailed, testError); + fireOnTestFailure(testFailedEvent); } public void visitPublishArtifacts(@NotNull final PublishArtifacts publishArtifacts) { @@ -425,7 +402,6 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer @Override public void visitMessageWithStatus(@NotNull Message msg) { - final String name = msg.getMessageName(); final Map msgAttrs = msg.getAttributes(); final String text = msgAttrs.get(ATTR_KEY_TEXT); diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/SMTestProxy.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/SMTestProxy.java index ef66990b4384..68464529f969 100644 --- a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/SMTestProxy.java +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/SMTestProxy.java @@ -61,12 +61,20 @@ public class SMTestProxy extends AbstractTestProxy { private boolean myIsEmptyIsCached = false; // is used for separating unknown and unset values private boolean myIsEmpty = true; TestLocationProvider myCustomLocator = null; + private final boolean myPreservePresentableName; public SMTestProxy(final String testName, final boolean isSuite, @Nullable final String locationUrl) { + this(testName, isSuite, locationUrl, false); + } + + public SMTestProxy(final String testName, final boolean isSuite, + @Nullable final String locationUrl, + boolean preservePresentableName) { myName = testName; myIsSuite = isSuite; myLocationUrl = locationUrl; + myPreservePresentableName = preservePresentableName; } public void setLocator(@NotNull TestLocationProvider locator) { @@ -470,6 +478,9 @@ public class SMTestProxy extends AbstractTestProxy { @NotNull public String getPresentableName() { + if (myPreservePresentableName) { + return TestsPresentationUtil.getPresentableNameTrimmedOnly(this); + } return TestsPresentationUtil.getPresentableName(this); } diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/BaseStartedNodeEvent.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/BaseStartedNodeEvent.java new file mode 100644 index 000000000000..9182d4619b8d --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/BaseStartedNodeEvent.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2012 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.execution.testframework.sm.runner.events; + +import jetbrains.buildServer.messages.serviceMessages.MessageWithAttributes; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Sergey Simonchik + */ +public abstract class BaseStartedNodeEvent extends TreeNodeEvent { + + private final int myParentId; + private final String myLocationUrl; + + protected BaseStartedNodeEvent(@NotNull String name, + int id, + int parentId, + @Nullable final String locationUrl) { + super(name, id); + myParentId = parentId; + myLocationUrl = locationUrl; + validate(); + } + + private void validate() { + if (myParentId < -1) { + fail("parentId should be greater than -2"); + } + if (getId() == -1 ^ myParentId == -1) { + fail("id and parentId should be -1 or non-negative"); + } + } + + /** + * @return parent node id (non-negative integer), or -1 if undefined + */ + public int getParentId() { + return myParentId; + } + + @Nullable + public String getLocationUrl() { + return myLocationUrl; + } + + @Override + protected void appendToStringInfo(@NotNull StringBuilder buf) { + append(buf, "parentId", myParentId); + append(buf, "locationUrl", myLocationUrl); + } + + public static int getParentNodeId(@NotNull MessageWithAttributes message) { + return TreeNodeEvent.getIntAttribute(message, "parentNodeId"); + } + +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestFailedEvent.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestFailedEvent.java new file mode 100644 index 000000000000..04ed7e004b29 --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestFailedEvent.java @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2012 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.execution.testframework.sm.runner.events; + +import com.google.common.base.Preconditions; +import jetbrains.buildServer.messages.serviceMessages.TestFailed; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Sergey Simonchik + */ +public class TestFailedEvent extends TreeNodeEvent { + + private final String myLocalizedFailureMessage; + private final String myStacktrace; + private final boolean myTestError; + private final String myComparisonFailureActualText; + private final String myComparisonFailureExpectedText; + + public TestFailedEvent(@NotNull TestFailed testFailed, boolean testError) { + super(testFailed.getTestName(), TreeNodeEvent.getNodeId(testFailed)); + myLocalizedFailureMessage = Preconditions.checkNotNull(testFailed.getFailureMessage()); + myStacktrace = testFailed.getStacktrace(); + myTestError = testError; + myComparisonFailureActualText = testFailed.getActual(); + myComparisonFailureExpectedText = testFailed.getExpected(); + } + + public TestFailedEvent(@NotNull String testName, + @NotNull String localizedFailureMessage, + @Nullable String stackTrace, + boolean testError, + @Nullable String comparisonFailureActualText, + @Nullable String comparisonFailureExpectedText) { + super(testName, -1); + myLocalizedFailureMessage = Preconditions.checkNotNull(localizedFailureMessage); + myStacktrace = stackTrace; + myTestError = testError; + myComparisonFailureActualText = comparisonFailureActualText; + myComparisonFailureExpectedText = comparisonFailureExpectedText; + } + + @NotNull + public String getLocalizedFailureMessage() { + return myLocalizedFailureMessage; + } + + @Nullable + public String getStacktrace() { + return myStacktrace; + } + + public boolean isTestError() { + return myTestError; + } + + @Nullable + public String getComparisonFailureActualText() { + return myComparisonFailureActualText; + } + + @Nullable + public String getComparisonFailureExpectedText() { + return myComparisonFailureExpectedText; + } + + @Override + protected void appendToStringInfo(@NotNull StringBuilder buf) { + append(buf, "localizedFailureMessage", myLocalizedFailureMessage); + append(buf, "stacktrace", myStacktrace); + append(buf, "isTestError", myTestError); + append(buf, "comparisonFailureActualText", myComparisonFailureActualText); + append(buf, "comparisonFailureExpectedText", myComparisonFailureExpectedText); + } + +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestFinishedEvent.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestFinishedEvent.java new file mode 100644 index 000000000000..02ebdda3b220 --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestFinishedEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2012 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.execution.testframework.sm.runner.events; + +import jetbrains.buildServer.messages.serviceMessages.TestFinished; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Sergey Simonchik + */ +public class TestFinishedEvent extends TreeNodeEvent { + + private final int myDuration; + + public TestFinishedEvent(@NotNull TestFinished testFinished, int duration) { + this(testFinished.getTestName(), TreeNodeEvent.getNodeId(testFinished), duration); + } + + public TestFinishedEvent(@Nullable String name, int id, int duration) { + super(name, id); + myDuration = duration; + } + + public TestFinishedEvent(@NotNull String name, int duration) { + this(name, -1, duration); + } + + public int getDuration() { + return myDuration; + } + + @Override + protected void appendToStringInfo(@NotNull StringBuilder buf) { + append(buf, "duration", myDuration); + } +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestIgnoredEvent.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestIgnoredEvent.java new file mode 100644 index 000000000000..2237a8670cf0 --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestIgnoredEvent.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2012 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.execution.testframework.sm.runner.events; + +import jetbrains.buildServer.messages.serviceMessages.TestIgnored; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Sergey Simonchik + */ +public class TestIgnoredEvent extends TreeNodeEvent { + private final String myIgnoreComment; + private final String myStacktrace; + + public TestIgnoredEvent(@NotNull String testName, @NotNull String ignoreComment, @Nullable String stacktrace) { + super(testName, -1); + myIgnoreComment = ignoreComment; + myStacktrace = stacktrace; + } + + public TestIgnoredEvent(@NotNull TestIgnored testIgnored, @Nullable String stacktrace) { + super(testIgnored.getTestName(), TreeNodeEvent.getNodeId(testIgnored)); + myIgnoreComment = testIgnored.getIgnoreComment(); + myStacktrace = stacktrace; + } + + @NotNull + public String getIgnoreComment() { + return myIgnoreComment; + } + + @Nullable + public String getStacktrace() { + return myStacktrace; + } + + @Override + protected void appendToStringInfo(@NotNull StringBuilder buf) { + append(buf, "ignoreComment", myIgnoreComment); + append(buf, "stacktrace", myStacktrace); + } +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestOutputEvent.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestOutputEvent.java new file mode 100644 index 000000000000..bc8297f6d03c --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestOutputEvent.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2012 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.execution.testframework.sm.runner.events; + +import jetbrains.buildServer.messages.serviceMessages.BaseTestMessage; +import org.jetbrains.annotations.NotNull; + +/** + * @author Sergey Simonchik + */ +public class TestOutputEvent extends TreeNodeEvent { + + private final String myText; + private final boolean myStdOut; + + public TestOutputEvent(@NotNull BaseTestMessage message, @NotNull String text, boolean stdOut) { + super(message.getTestName(), TreeNodeEvent.getNodeId(message)); + myText = text; + myStdOut = stdOut; + } + + public TestOutputEvent(@NotNull String testName, @NotNull String text, boolean stdOut) { + super(testName, -1); + myText = text; + myStdOut = stdOut; + } + + @NotNull + public String getText() { + return myText; + } + + public boolean isStdOut() { + return myStdOut; + } + + @Override + protected void appendToStringInfo(@NotNull StringBuilder buf) { + append(buf, "text", myText); + append(buf, "stdOut", myStdOut); + } +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestStartedEvent.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestStartedEvent.java new file mode 100644 index 000000000000..05ba2638e401 --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestStartedEvent.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2012 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.execution.testframework.sm.runner.events; + +import jetbrains.buildServer.messages.serviceMessages.TestStarted; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Sergey Simonchik + */ +public class TestStartedEvent extends BaseStartedNodeEvent { + + public TestStartedEvent(@NotNull TestStarted testStarted, + @Nullable String locationUrl) { + super(testStarted.getTestName(), TreeNodeEvent.getNodeId(testStarted), + getParentNodeId(testStarted), locationUrl); + } + + public TestStartedEvent(@NotNull String name, @Nullable String locationUrl) { + super(name, -1, -1, locationUrl); + } +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestSuiteFinishedEvent.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestSuiteFinishedEvent.java new file mode 100644 index 000000000000..e5a6e34853f9 --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestSuiteFinishedEvent.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2012 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.execution.testframework.sm.runner.events; + +import jetbrains.buildServer.messages.serviceMessages.TestSuiteFinished; +import org.jetbrains.annotations.NotNull; + +/** + * @author Sergey Simonchik + */ +public class TestSuiteFinishedEvent extends TreeNodeEvent { + + public TestSuiteFinishedEvent(@NotNull TestSuiteFinished suiteFinished) { + super(suiteFinished.getSuiteName(), TreeNodeEvent.getNodeId(suiteFinished)); + } + + public TestSuiteFinishedEvent(@NotNull String name) { + super(name, -1); + } + + @Override + protected void appendToStringInfo(@NotNull StringBuilder buf) { + } +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestSuiteStartedEvent.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestSuiteStartedEvent.java new file mode 100644 index 000000000000..4c761e7471a1 --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TestSuiteStartedEvent.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2012 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.execution.testframework.sm.runner.events; + +import jetbrains.buildServer.messages.serviceMessages.TestSuiteStarted; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Sergey Simonchik + */ +public class TestSuiteStartedEvent extends BaseStartedNodeEvent { + + public TestSuiteStartedEvent(@NotNull TestSuiteStarted suiteStarted, + @Nullable String locationUrl) { + super(suiteStarted.getSuiteName(), TreeNodeEvent.getNodeId(suiteStarted), + getParentNodeId(suiteStarted), locationUrl); + } + + public TestSuiteStartedEvent(@NotNull String name, @Nullable String locationUrl) { + super(name, -1, -1, locationUrl); + } + +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TreeNodeEvent.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TreeNodeEvent.java new file mode 100644 index 000000000000..2758e5536bb1 --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/events/TreeNodeEvent.java @@ -0,0 +1,101 @@ +/* + * Copyright 2000-2012 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.execution.testframework.sm.runner.events; + +import jetbrains.buildServer.messages.serviceMessages.MessageWithAttributes; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Sergey Simonchik + */ +public abstract class TreeNodeEvent { + + private final String myName; + private final int myId; + + public TreeNodeEvent(@Nullable String name, int id) { + myName = name; + myId = id; + validate(); + } + + private void validate() { + if (myId < -1) { + fail("id should be greater than -2"); + } + if (myName != null && myName.isEmpty()) { + fail("Tree node name is empty"); + } + } + + protected void fail(@NotNull String message) { + throw new IllegalStateException(message + ", " + toString()); + } + + @Nullable + public String getName() { + return myName; + } + + /** + * @return tree node id (non-negative integer), or -1 if undefined + */ + public int getId() { + return myId; + } + + @Override + public final String toString() { + StringBuilder buf = new StringBuilder(getClass().getSimpleName() + "{"); + append(buf, "name", myName); + append(buf, "id", myId); + appendToStringInfo(buf); + // drop last 2 chars: ', ' + buf.setLength(buf.length() - 2); + buf.append("}"); + return buf.toString(); + } + + protected abstract void appendToStringInfo(@NotNull StringBuilder buf); + + protected static void append(@NotNull StringBuilder buffer, + @NotNull String key, @Nullable Object value) { + if (value != null) { + buffer.append(key).append("="); + if (value instanceof String) { + buffer.append("'").append(value).append("'"); + } + else { + buffer.append(String.valueOf(value)); + } + buffer.append(", "); + } + } + + public static int getNodeId(@NotNull MessageWithAttributes message) { + return getIntAttribute(message, "nodeId"); + } + + public static int getIntAttribute(@NotNull MessageWithAttributes message, @NotNull String key) { + String value = message.getAttributes().get(key); + if (value == null) { + return -1; + } + return Integer.parseInt(value); + } + +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/TestsPresentationUtil.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/TestsPresentationUtil.java index c3f1b922761d..0e97d51729f5 100644 --- a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/TestsPresentationUtil.java +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/TestsPresentationUtil.java @@ -226,6 +226,18 @@ public class TestsPresentationUtil { return presentationCandidate; } + @NotNull + public static String getPresentableNameTrimmedOnly(@NotNull SMTestProxy testProxy) { + String name = testProxy.getName(); + if (name != null) { + name = name.trim(); + } + if (name == null || name.isEmpty()) { + name = NO_NAME_TEST; + } + return name; + } + @Nullable private static Icon getIcon(final SMTestProxy testProxy, final TestConsoleProperties consoleProperties) { diff --git a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/GeneralToSMTRunnerEventsConvertorTest.java b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/GeneralToSMTRunnerEventsConvertorTest.java index 2b3677e3fbfa..933972021fdf 100644 --- a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/GeneralToSMTRunnerEventsConvertorTest.java +++ b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/GeneralToSMTRunnerEventsConvertorTest.java @@ -18,6 +18,7 @@ package com.intellij.execution.testframework.sm.runner; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.testframework.TestConsoleProperties; import com.intellij.execution.testframework.sm.Marker; +import com.intellij.execution.testframework.sm.runner.events.*; import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView; import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerTestTreeView; import com.intellij.execution.testframework.sm.runner.ui.SMTestRunnerResultsForm; @@ -125,7 +126,7 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase public void testOnTestFailure() { onTestStarted("some_test"); - myEventsProcessor.onTestFailure("some_test", "", "", false, null, null); + myEventsProcessor.onTestFailure(new TestFailedEvent("some_test", "", "", false, null, null)); final String fullName = myEventsProcessor.getFullTestName("some_test"); final SMTestProxy proxy = myEventsProcessor.getProxyByFullTestName(fullName); @@ -136,7 +137,7 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase public void testOnTestComparisionFailure() { onTestStarted("some_test"); - myEventsProcessor.onTestFailure("some_test", "", "", false, "actual", "expected"); + myEventsProcessor.onTestFailure(new TestFailedEvent("some_test", "", "", false, "actual", "expected")); final String fullName = myEventsProcessor.getFullTestName("some_test"); final SMTestProxy proxy = myEventsProcessor.getProxyByFullTestName(fullName); @@ -147,8 +148,8 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase public void testOnTestFailure_Twice() { onTestStarted("some_test"); - myEventsProcessor.onTestFailure("some_test", "", "", false, null, null); - myEventsProcessor.onTestFailure("some_test", "", "", false, null, null); + myEventsProcessor.onTestFailure(new TestFailedEvent("some_test", "", "", false, null, null)); + myEventsProcessor.onTestFailure(new TestFailedEvent("some_test", "", "", false, null, null)); assertEquals(1, myEventsProcessor.getRunningTestsQuantity()); assertEquals(1, myEventsProcessor.getFailedTestsSet().size()); @@ -156,7 +157,7 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase public void testOnTestError() { onTestStarted("some_test"); - myEventsProcessor.onTestFailure("some_test", "", "", true, null, null); + myEventsProcessor.onTestFailure(new TestFailedEvent("some_test", "", "", true, null, null)); final String fullName = myEventsProcessor.getFullTestName("some_test"); final SMTestProxy proxy = myEventsProcessor.getProxyByFullTestName(fullName); @@ -167,7 +168,7 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase public void testOnTestIgnored() { onTestStarted("some_test"); - myEventsProcessor.onTestIgnored("some_test", "", null); + myEventsProcessor.onTestIgnored(new TestIgnoredEvent("some_test", "", null)); final String fullName = myEventsProcessor.getFullTestName("some_test"); final SMTestProxy proxy = myEventsProcessor.getProxyByFullTestName(fullName); @@ -180,7 +181,7 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase onTestStarted("some_test"); final String fullName = myEventsProcessor.getFullTestName("some_test"); final SMTestProxy proxy = myEventsProcessor.getProxyByFullTestName(fullName); - myEventsProcessor.onTestFinished("some_test", 10); + myEventsProcessor.onTestFinished(new TestFinishedEvent("some_test", 10)); assertEquals(0, myEventsProcessor.getRunningTestsQuantity()); assertEquals(0, myEventsProcessor.getFailedTestsSet().size()); @@ -224,8 +225,8 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase public void testOnFinishedTesting_WithFailure() { onTestStarted("test"); - myEventsProcessor.onTestFailure("test", "", "", false, null, null); - myEventsProcessor.onTestFinished("test", 10); + myEventsProcessor.onTestFailure(new TestFailedEvent("test", "", "", false, null, null)); + myEventsProcessor.onTestFinished(new TestFinishedEvent("test", 10)); myEventsProcessor.onFinishTesting(); //Tree @@ -240,8 +241,8 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase public void testOnFinishedTesting_WithError() { onTestStarted("test"); - myEventsProcessor.onTestFailure("test", "", "", true, null, null); - myEventsProcessor.onTestFinished("test", 10); + myEventsProcessor.onTestFailure(new TestFailedEvent("test", "", "", true, null, null)); + myEventsProcessor.onTestFinished(new TestFinishedEvent("test", 10)); myEventsProcessor.onFinishTesting(); //Tree @@ -256,8 +257,8 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase public void testOnFinishedTesting_WithIgnored() { onTestStarted("test"); - myEventsProcessor.onTestIgnored("test", "", null); - myEventsProcessor.onTestFinished("test", 10); + myEventsProcessor.onTestIgnored(new TestIgnoredEvent("test", "", null)); + myEventsProcessor.onTestFinished(new TestFinishedEvent("test", 10)); myEventsProcessor.onFinishTesting(); //Tree @@ -302,18 +303,18 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase assertEquals("suite3", test2.getParent().getName()); assertEquals("suite2", test2.getParent().getParent().getName()); - myEventsProcessor.onTestFinished("test2", 10); + myEventsProcessor.onTestFinished(new TestFinishedEvent("test2", 10)); //check that after finishing suite (suite3), current will be parent of finished suite (i.e. suite2) - myEventsProcessor.onSuiteFinished("suite3"); + myEventsProcessor.onSuiteFinished(new TestSuiteFinishedEvent("suite3")); onTestStarted("test3"); final SMTestProxy test3 = myEventsProcessor.getProxyByFullTestName(myEventsProcessor.getFullTestName("test3")); assertEquals("suite2", test3.getParent().getName()); //clean up - myEventsProcessor.onSuiteFinished("suite2"); - myEventsProcessor.onSuiteFinished("suite1"); + myEventsProcessor.onSuiteFinished(new TestSuiteFinishedEvent("suite2")); + myEventsProcessor.onSuiteFinished(new TestSuiteFinishedEvent("suite1")); } public void testOnSuiteStarted_WithLocation() { @@ -336,17 +337,17 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase } public void testConcurrentSuite_intersected() { - myEventsProcessor.onSuiteStarted("suite1", null); - myEventsProcessor.onTestStarted("suite2.test1", null); + myEventsProcessor.onSuiteStarted(new TestSuiteStartedEvent("suite1", null)); + myEventsProcessor.onTestStarted(new TestStartedEvent("suite2.test1", null)); final SMTestProxy test1 = myEventsProcessor.getProxyByFullTestName(myEventsProcessor.getFullTestName("suite2.test1")); - myEventsProcessor.onSuiteFinished("suite1"); + myEventsProcessor.onSuiteFinished(new TestSuiteFinishedEvent("suite1")); - myEventsProcessor.onSuiteStarted("suite2", null); - myEventsProcessor.onTestFinished("suite2.test1", 10); - myEventsProcessor.onSuiteFinished("suite2"); + myEventsProcessor.onSuiteStarted(new TestSuiteStartedEvent("suite2", null)); + myEventsProcessor.onTestFinished(new TestFinishedEvent("suite2.test1", 10)); + myEventsProcessor.onSuiteFinished(new TestSuiteFinishedEvent("suite2")); assertEquals("suite1", test1.getParent().getName()); @@ -371,7 +372,7 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase } private void onTestStarted(final String testName, @Nullable final String locationUrl) { - myEventsProcessor.onTestStarted(testName, locationUrl); + myEventsProcessor.onTestStarted(new TestStartedEvent(testName, locationUrl)); myResultsViewer.performUpdate(); } @@ -380,7 +381,7 @@ public class GeneralToSMTRunnerEventsConvertorTest extends BaseSMTRunnerTestCase } private void onTestSuiteStarted(final String suiteName, @Nullable final String locationUrl) { - myEventsProcessor.onSuiteStarted(suiteName, locationUrl); + myEventsProcessor.onSuiteStarted(new TestSuiteStartedEvent(suiteName, locationUrl)); myResultsViewer.performUpdate(); } } diff --git a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/MockGeneralTestEventsProcessorAdapter.java b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/MockGeneralTestEventsProcessorAdapter.java index 86289ef956ef..822e4e11520b 100644 --- a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/MockGeneralTestEventsProcessorAdapter.java +++ b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/MockGeneralTestEventsProcessorAdapter.java @@ -15,7 +15,9 @@ */ package com.intellij.execution.testframework.sm.runner; +import com.intellij.execution.testframework.sm.runner.events.*; import com.intellij.openapi.util.Key; +import com.intellij.testIntegration.TestLocationProvider; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -24,46 +26,46 @@ import org.jetbrains.annotations.Nullable; */ public class MockGeneralTestEventsProcessorAdapter implements GeneralTestEventsProcessor { private final StringBuilder myOutputBuffer = new StringBuilder(); + + @Override + public void onStartTesting() { + } + @Override public void onTestsCountInSuite(int count) { } @Override - public void onTestStarted(@NotNull String testName, @Nullable String locationUrl) { + public void onTestStarted(@NotNull TestStartedEvent testStartedEvent) { } @Override - public void onTestFinished(@NotNull String testName, int duration) { + public void onTestFinished(@NotNull TestFinishedEvent testFinishedEvent) { } @Override - public void onTestFailure(@NotNull String testName, - @NotNull String localizedMessage, - @Nullable String stackTrace, - boolean testError, - @Nullable String comparisionFailureActualText, - @Nullable String comparisionFailureExpectedText) { + public void onTestFailure(@NotNull TestFailedEvent testFailedEvent) { } @Override - public void onTestIgnored(@NotNull String testName, @NotNull String ignoreComment, @Nullable String stackTrace) { + public void onTestIgnored(@NotNull TestIgnoredEvent testIgnoredEvent) { } @Override - public void onTestOutput(@NotNull String testName, @NotNull String text, boolean stdOut) { + public void onTestOutput(@NotNull TestOutputEvent testOutputEvent) { } @Override - public void onSuiteStarted(@NotNull String suiteName, @Nullable String locationUrl) { + public void onSuiteStarted(@NotNull TestSuiteStartedEvent suiteStartedEvent) { } @Override - public void onSuiteFinished(@NotNull String suiteName) { + public void onSuiteFinished(@NotNull TestSuiteFinishedEvent suiteFinishedEvent) { } @Override public void onUncapturedOutput(@NotNull String text, Key outputType) { - myOutputBuffer.append("[").append(outputType.toString()).append("]"+ text); + myOutputBuffer.append("[").append(outputType.toString()).append("]").append(text); } @Override @@ -86,6 +88,18 @@ public class MockGeneralTestEventsProcessorAdapter implements GeneralTestEventsP public void onTestsReporterAttached() { } + @Override + public void setLocator(@NotNull TestLocationProvider locator) { + } + + @Override + public void addEventsListener(@NotNull SMTRunnerEventsListener viewer) { + } + + @Override + public void onFinishTesting() { + } + @Override public void dispose() { myOutputBuffer.setLength(0); diff --git a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/SMTRunnerConsoleTest.java b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/SMTRunnerConsoleTest.java index 7f345863b0ad..07982eb04d30 100644 --- a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/SMTRunnerConsoleTest.java +++ b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/SMTRunnerConsoleTest.java @@ -22,6 +22,7 @@ import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.testframework.Printable; import com.intellij.execution.testframework.Printer; import com.intellij.execution.testframework.TestConsoleProperties; +import com.intellij.execution.testframework.sm.runner.events.*; import com.intellij.execution.testframework.sm.runner.ui.MockPrinter; import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView; import com.intellij.execution.testframework.sm.runner.ui.SMTestRunnerResultsForm; @@ -174,8 +175,8 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_OnTestStdOutput() { startTestWithPrinter("my_test"); - myEventsProcessor.onTestOutput("my_test", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test", "stdout2", true); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout2", true)); assertStdOutput(myMockResetablePrinter, "stdout1 stdout2"); } @@ -183,8 +184,8 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_OnTestStdErr() { startTestWithPrinter("my_test"); - myEventsProcessor.onTestOutput("my_test", "stderr1 ", false); - myEventsProcessor.onTestOutput("my_test", "stderr2", false); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr1 ", false)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr2", false)); assertStdErr(myMockResetablePrinter, "stderr1 stderr2"); } @@ -192,10 +193,10 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_OnTestMixedStd() { startTestWithPrinter("my_test"); - myEventsProcessor.onTestOutput("my_test", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test", "stderr1 ", false); - myEventsProcessor.onTestOutput("my_test", "stdout2", true); - myEventsProcessor.onTestOutput("my_test", "stderr2", false); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr1 ", false)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout2", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr2", false)); assertAllOutputs(myMockResetablePrinter, "stdout1 stdout2", "stderr1 stderr2", ""); } @@ -203,9 +204,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_OnFailure() { final SMTestProxy myTest1 = startTestWithPrinter("my_test"); - myEventsProcessor.onTestFailure("my_test", "error msg", "method1:1\nmethod2:2", false, null, null); - myEventsProcessor.onTestOutput("my_test", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test", "stderr1 ", false); + myEventsProcessor.onTestFailure(new TestFailedEvent("my_test", "error msg", "method1:1\nmethod2:2", false, null, null)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr1 ", false)); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "\nerror msg\nmethod1:1\nmethod2:2\nstderr1 ", ""); @@ -215,9 +216,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { //other output order final SMTestProxy myTest2 = startTestWithPrinter("my_test2"); - myEventsProcessor.onTestOutput("my_test2", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test2", "stderr1 ", false); - myEventsProcessor.onTestFailure("my_test2", "error msg", "method1:1\nmethod2:2", false, null, null); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test2", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test2", "stderr1 ", false)); + myEventsProcessor.onTestFailure(new TestFailedEvent("my_test2", "error msg", "method1:1\nmethod2:2", false, null, null)); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "stderr1 \nerror msg\nmethod1:1\nmethod2:2\n", ""); final MockPrinter mockPrinter2 = new MockPrinter(true); @@ -228,9 +229,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_OnFailure_EmptyStacktrace() { final SMTestProxy myTest1 = startTestWithPrinter("my_test"); - myEventsProcessor.onTestFailure("my_test", "error msg", "\n\n", false, null, null); - myEventsProcessor.onTestOutput("my_test", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test", "stderr1 ", false); + myEventsProcessor.onTestFailure(new TestFailedEvent("my_test", "error msg", "\n\n", false, null, null)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr1 ", false)); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "\nerror msg\nstderr1 ", ""); @@ -242,9 +243,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_OnFailure_Comparision_Strings() { final SMTestProxy myTest1 = startTestWithPrinter("my_test"); - myEventsProcessor.onTestFailure("my_test", "error msg", "method1:1\nmethod2:2", false, "actual", "expected"); - myEventsProcessor.onTestOutput("my_test", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test", "stderr1 ", false); + myEventsProcessor.onTestFailure(new TestFailedEvent("my_test", "error msg", "method1:1\nmethod2:2", false, "actual", "expected")); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr1 ", false)); assertAllOutputs(myMockResetablePrinter, // std out @@ -281,10 +282,10 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_OnFailure_Comparision_MultilineTexts() { final SMTestProxy myTest1 = startTestWithPrinter("my_test"); - myEventsProcessor.onTestFailure("my_test", "error msg", "method1:1\nmethod2:2", false, - "this is:\nactual", "this is:\nexpected"); - myEventsProcessor.onTestOutput("my_test", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test", "stderr1 ", false); + myEventsProcessor.onTestFailure(new TestFailedEvent("my_test", "error msg", "method1:1\nmethod2:2", false, + "this is:\nactual", "this is:\nexpected")); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr1 ", false)); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "\nerror msg \n" + "\n" + @@ -304,9 +305,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_OnError() { final SMTestProxy myTest1 = startTestWithPrinter("my_test"); - myEventsProcessor.onTestFailure("my_test", "error msg", "method1:1\nmethod2:2", true, null, null); - myEventsProcessor.onTestOutput("my_test", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test", "stderr1 ", false); + myEventsProcessor.onTestFailure(new TestFailedEvent("my_test", "error msg", "method1:1\nmethod2:2", true, null, null)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr1 ", false)); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "\nerror msg\nmethod1:1\nmethod2:2\nstderr1 ", ""); @@ -316,9 +317,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { //other output order final SMTestProxy myTest2 = startTestWithPrinter("my_test2"); - myEventsProcessor.onTestOutput("my_test2", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test2", "stderr1 ", false); - myEventsProcessor.onTestFailure("my_test2", "error msg", "method1:1\nmethod2:2", true, null, null); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test2", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test2", "stderr1 ", false)); + myEventsProcessor.onTestFailure(new TestFailedEvent("my_test2", "error msg", "method1:1\nmethod2:2", true, null, null)); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "stderr1 \nerror msg\nmethod1:1\nmethod2:2\n", ""); final MockPrinter mockPrinter2 = new MockPrinter(true); @@ -330,8 +331,8 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { final SMTestProxy myTest1 = startTestWithPrinter("my_test"); myEventsProcessor.onError("error msg", "method1:1\nmethod2:2", true); - myEventsProcessor.onTestOutput("my_test", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test", "stderr1 ", false); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr1 ", false)); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "\nerror msg\nmethod1:1\nmethod2:2\nstderr1 ", ""); @@ -342,13 +343,13 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { "method1:1\n" + "method2:2\n" + "stderr1 ", ""); - myEventsProcessor.onTestFinished("my_test", 1); + myEventsProcessor.onTestFinished(new TestFinishedEvent("my_test", 1)); myTest1.setFinished(); //other output order final SMTestProxy myTest2 = startTestWithPrinter("my_test2"); - myEventsProcessor.onTestOutput("my_test2", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test2", "stderr1 ", false); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test2", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test2", "stderr1 ", false)); myEventsProcessor.onError("error msg", "method1:1\nmethod2:2", true); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "stderr1 \nerror msg\nmethod1:1\nmethod2:2\n", ""); @@ -360,7 +361,7 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_Suite_OnErrorMsg() { myEventsProcessor.onError("error msg:root", "method1:1\nmethod2:2", true); - myEventsProcessor.onSuiteStarted("suite", null); + myEventsProcessor.onSuiteStarted(new TestSuiteStartedEvent("suite", null)); final SMTestProxy suite = myEventsProcessor.getCurrentSuite(); suite.setPrinter(myMockResetablePrinter); myEventsProcessor.onError("error msg:suite", "method1:1\nmethod2:2", true); @@ -391,9 +392,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_OnIgnored() { final SMTestProxy myTest1 = startTestWithPrinter("my_test"); - myEventsProcessor.onTestIgnored("my_test", "ignored msg", null); - myEventsProcessor.onTestOutput("my_test", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test", "stderr1 ", false); + myEventsProcessor.onTestIgnored(new TestIgnoredEvent("my_test", "ignored msg", null)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr1 ", false)); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "stderr1 ", "\nignored msg\n"); @@ -403,9 +404,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { //other output order final SMTestProxy myTest2 = startTestWithPrinter("my_test2"); - myEventsProcessor.onTestOutput("my_test2", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test2", "stderr1 ", false); - myEventsProcessor.onTestIgnored("my_test2", "ignored msg", null); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test2", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test2", "stderr1 ", false)); + myEventsProcessor.onTestIgnored(new TestIgnoredEvent("my_test2", "ignored msg", null)); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "stderr1 ", "\nignored msg\n"); final MockPrinter mockPrinter2 = new MockPrinter(true); @@ -416,9 +417,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testProcessor_OnIgnored_WithStacktrace() { final SMTestProxy myTest1 = startTestWithPrinter("my_test"); - myEventsProcessor.onTestIgnored("my_test", "ignored2 msg", "method1:1\nmethod2:2"); - myEventsProcessor.onTestOutput("my_test", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test", "stderr1 ", false); + myEventsProcessor.onTestIgnored(new TestIgnoredEvent("my_test", "ignored2 msg", "method1:1\nmethod2:2")); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test", "stderr1 ", false)); assertAllOutputs(myMockResetablePrinter, "stdout1 ", "\nmethod1:1\nmethod2:2\nstderr1 ", @@ -433,9 +434,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { //other output order final SMTestProxy myTest2 = startTestWithPrinter("my_test2"); - myEventsProcessor.onTestOutput("my_test2", "stdout1 ", true); - myEventsProcessor.onTestOutput("my_test2", "stderr1 ", false); - myEventsProcessor.onTestIgnored("my_test2", "ignored msg", "method1:1\nmethod2:2"); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test2", "stdout1 ", true)); + myEventsProcessor.onTestOutput(new TestOutputEvent("my_test2", "stderr1 ", false)); + myEventsProcessor.onTestIgnored(new TestIgnoredEvent("my_test2", "ignored msg", "method1:1\nmethod2:2")); assertAllOutputs(myMockResetablePrinter, "stdout1 ", @@ -465,7 +466,7 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testOnUncapturedOutput_SomeSuite() { myEventsProcessor.onStartTesting(); - myEventsProcessor.onSuiteStarted("my suite", null); + myEventsProcessor.onSuiteStarted(new TestSuiteStartedEvent("my suite", null)); final SMTestProxy mySuite = myEventsProcessor.getCurrentSuite(); assertTrue(mySuite != myRootSuite); mySuite.setPrinter(myMockResetablePrinter); @@ -476,7 +477,7 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { public void testOnUncapturedOutput_SomeTest() { myEventsProcessor.onStartTesting(); - myEventsProcessor.onSuiteStarted("my suite", null); + myEventsProcessor.onSuiteStarted(new TestSuiteStartedEvent("my suite", null)); startTestWithPrinter("my test"); assertOnUncapturedOutput(); @@ -514,9 +515,9 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { myConsole.attachToProcess(null); myEventsProcessor.onStartTesting(); - myEventsProcessor.onSuiteStarted("suite", null); + myEventsProcessor.onSuiteStarted(new TestSuiteStartedEvent("suite", null)); final SMTestProxy suite = myEventsProcessor.getCurrentSuite(); - myEventsProcessor.onSuiteFinished("suite"); + myEventsProcessor.onSuiteFinished(new TestSuiteFinishedEvent("suite")); myEventsProcessor.onUncapturedOutput("preved", ProcessOutputTypes.STDOUT); myEventsProcessor.onFinishTesting(); @@ -536,7 +537,7 @@ public class SMTRunnerConsoleTest extends BaseSMTRunnerTestCase { } private SMTestProxy startTestWithPrinter(final String testName) { - myEventsProcessor.onTestStarted(testName, null); + myEventsProcessor.onTestStarted(new TestStartedEvent(testName, null)); final SMTestProxy proxy = myEventsProcessor.getProxyByFullTestName(myEventsProcessor.getFullTestName(testName)); proxy.setPrinter(myMockResetablePrinter); diff --git a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/ui/SMTestRunnerResultsFormTest.java b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/ui/SMTestRunnerResultsFormTest.java index 6ed0317ee996..b4aa1ee7148c 100644 --- a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/ui/SMTestRunnerResultsFormTest.java +++ b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/runner/ui/SMTestRunnerResultsFormTest.java @@ -21,6 +21,7 @@ import com.intellij.execution.testframework.sm.Marker; import com.intellij.execution.testframework.sm.runner.BaseSMTRunnerTestCase; import com.intellij.execution.testframework.sm.runner.GeneralToSMTRunnerEventsConvertor; import com.intellij.execution.testframework.sm.runner.SMTestProxy; +import com.intellij.execution.testframework.sm.runner.events.*; import com.intellij.openapi.progress.util.ColorProgressBar; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Ref; @@ -226,24 +227,24 @@ public class SMTestRunnerResultsFormTest extends BaseSMTRunnerTestCase { TestConsoleProperties.HIDE_PASSED_TESTS.set(myConsoleProperties, true); myEventsProcessor.onStartTesting(); - myEventsProcessor.onSuiteStarted("suite", null); + myEventsProcessor.onSuiteStarted(new TestSuiteStartedEvent("suite", null)); myResultsViewer.performUpdate(); - myEventsProcessor.onTestStarted("test_failed", null); + myEventsProcessor.onTestStarted(new TestStartedEvent("test_failed", null)); myResultsViewer.performUpdate(); - myEventsProcessor.onTestFailure("test_failed", "", "", false, null, null); + myEventsProcessor.onTestFailure(new TestFailedEvent("test_failed", "", "", false, null, null)); myResultsViewer.performUpdate(); - myEventsProcessor.onTestFinished("test_failed", 10); + myEventsProcessor.onTestFinished(new TestFinishedEvent("test_failed", 10)); myResultsViewer.performUpdate(); - myEventsProcessor.onTestStarted("test", null); + myEventsProcessor.onTestStarted(new TestStartedEvent("test", null)); myResultsViewer.performUpdate(); assertEquals(2, myTreeModel.getChildCount(myTreeModel.getChild(myTreeModel.getRoot(), 0))); - myEventsProcessor.onTestFinished("test", 10); + myEventsProcessor.onTestFinished(new TestFinishedEvent("test", 10)); assertEquals(2, myTreeModel.getChildCount(myTreeModel.getChild(myTreeModel.getRoot(), 0))); - myEventsProcessor.onSuiteFinished("suite"); + myEventsProcessor.onSuiteFinished(new TestSuiteFinishedEvent("suite")); myEventsProcessor.onFinishTesting(); assertEquals(1, myTreeModel.getChildCount(myTreeModel.getChild(myTreeModel.getRoot(), 0))); @@ -251,27 +252,27 @@ public class SMTestRunnerResultsFormTest extends BaseSMTRunnerTestCase { public void testExpandIfOnlyOneRootChild() throws InterruptedException { myEventsProcessor.onStartTesting(); - myEventsProcessor.onSuiteStarted("suite1", null); + myEventsProcessor.onSuiteStarted(new TestSuiteStartedEvent("suite1", null)); myResultsViewer.performUpdate(); - myEventsProcessor.onSuiteStarted("suite2", null); + myEventsProcessor.onSuiteStarted(new TestSuiteStartedEvent("suite2", null)); myResultsViewer.performUpdate(); - myEventsProcessor.onTestStarted("test_failed", null); + myEventsProcessor.onTestStarted(new TestStartedEvent("test_failed", null)); myResultsViewer.performUpdate(); - myEventsProcessor.onTestFailure("test_failed", "", "", false, null, null); + myEventsProcessor.onTestFailure(new TestFailedEvent("test_failed", "", "", false, null, null)); myResultsViewer.performUpdate(); - myEventsProcessor.onTestFinished("test_failed", 10); + myEventsProcessor.onTestFinished(new TestFinishedEvent("test_failed", 10)); myResultsViewer.performUpdate(); - myEventsProcessor.onTestStarted("test", null); + myEventsProcessor.onTestStarted(new TestStartedEvent("test", null)); myResultsViewer.performUpdate(); - myEventsProcessor.onTestFinished("test", 10); + myEventsProcessor.onTestFinished(new TestFinishedEvent("test", 10)); myResultsViewer.performUpdate(); - myEventsProcessor.onSuiteFinished("suite2"); + myEventsProcessor.onSuiteFinished(new TestSuiteFinishedEvent("suite2")); myResultsViewer.performUpdate(); - myEventsProcessor.onSuiteFinished("suite1"); + myEventsProcessor.onSuiteFinished(new TestSuiteFinishedEvent("suite1")); myResultsViewer.performUpdate(); myEventsProcessor.onFinishTesting(); myResultsViewer.performUpdate(); diff --git a/plugins/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java b/plugins/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java index 6c90602ad3d3..df69b76e40f6 100644 --- a/plugins/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java +++ b/plugins/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java @@ -15,6 +15,7 @@ */ package org.jetbrains.android.util; +import com.android.jarutils.SignedJarBuilder; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.sdklib.IAndroidTarget; @@ -38,6 +39,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.*; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; @@ -90,6 +94,8 @@ public class AndroidCommonUtils { @NonNls public static final String PACKAGE_MANIFEST_ATTRIBUTE = "package"; + @NonNls public static final String ANDROID_FINAL_PACKAGE_FOR_ARTIFACT_SUFFIX = ".afp"; + private AndroidCommonUtils() { } @@ -529,4 +535,32 @@ public class AndroidCommonUtils { final String b = path.substring(dot); return a + suffix + b; } + + public static void signApk(@NotNull File srcApk, + @NotNull File destFile, + @NotNull PrivateKey privateKey, + @NotNull X509Certificate certificate) + throws IOException, GeneralSecurityException { + FileOutputStream fos = new FileOutputStream(destFile); + SignedJarBuilder builder = new SafeSignedJarBuilder(fos, privateKey, certificate, destFile.getPath()); + FileInputStream fis = new FileInputStream(srcApk); + try { + builder.writeZip(fis, null); + builder.close(); + } + finally { + try { + fis.close(); + } + catch (IOException ignored) { + } + finally { + try { + fos.close(); + } + catch (IOException ignored) { + } + } + } + } } diff --git a/plugins/android/src/META-INF/plugin.xml b/plugins/android/src/META-INF/plugin.xml index 23eb2b596175..3315888cb0df 100644 --- a/plugins/android/src/META-INF/plugin.xml +++ b/plugins/android/src/META-INF/plugin.xml @@ -219,6 +219,11 @@ + + + + + diff --git a/plugins/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java b/plugins/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java index 10551667f969..0875a4fb4be9 100644 --- a/plugins/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java +++ b/plugins/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java @@ -44,6 +44,9 @@ import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.*; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.artifacts.ArtifactProperties; +import com.intellij.packaging.impl.compiler.ArtifactCompileScope; import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiFile; @@ -52,6 +55,9 @@ import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.HashMap; import com.intellij.util.containers.HashSet; +import org.jetbrains.android.compiler.artifact.AndroidApplicationArtifactProperties; +import org.jetbrains.android.compiler.artifact.AndroidArtifactPropertiesProvider; +import org.jetbrains.android.compiler.artifact.AndroidArtifactSigningMode; import org.jetbrains.android.dom.manifest.Manifest; import org.jetbrains.android.dom.resources.Attr; import org.jetbrains.android.dom.resources.DeclareStyleable; @@ -86,6 +92,7 @@ public class AndroidCompileUtil { @NonNls public static final String PROGUARD_CFG_FILE_NAME = "proguard-project.txt"; @NonNls public static final String OLD_PROGUARD_CFG_FILE_NAME = "proguard.cfg"; + public static final String UNSIGNED_SUFFIX = ".unsigned"; private AndroidCompileUtil() { } @@ -131,7 +138,7 @@ public class AndroidCompileUtil { return null; } - static void addMessages(final CompileContext context, final Map> messages, Module module) { + public static void addMessages(final CompileContext context, final Map> messages, @Nullable Module module) { addMessages(context, messages, null, module); } @@ -150,7 +157,7 @@ public class AndroidCompileUtil { static void addMessages(final CompileContext context, final Map> messages, @Nullable final Map presentableFilesMap, - final Module module) { + @Nullable final Module module) { ApplicationManager.getApplication().runReadAction(new Runnable() { public void run() { if (context.getProject().isDisposed()) return; @@ -167,7 +174,7 @@ public class AndroidCompileUtil { line = Integer.parseInt(matcher.group(2)); } } - context.addMessage(category, '[' + module.getName() + "] " + message, url, line, -1); + context.addMessage(category, (module != null ? '[' + module.getName() + "] " : "") + message, url, line, -1); } } } @@ -619,7 +626,22 @@ public class AndroidCompileUtil { public static boolean isReleaseBuild(@NotNull CompileContext context) { final Boolean value = context.getCompileScope().getUserData(RELEASE_BUILD_KEY); - return value != null && value.booleanValue(); + if (value != null && value.booleanValue()) { + return true; + } + final Project project = context.getProject(); + final Set artifacts = ArtifactCompileScope.getArtifactsToBuild(project, context.getCompileScope(), false); + + if (artifacts != null) { + for (Artifact artifact : artifacts) { + final ArtifactProperties properties = artifact.getProperties(AndroidArtifactPropertiesProvider.getInstance()); + if (properties instanceof AndroidApplicationArtifactProperties && + ((AndroidApplicationArtifactProperties)properties).getSigningMode() != AndroidArtifactSigningMode.DEBUG) { + return true; + } + } + } + return false; } public static void setReleaseBuild(@NotNull CompileScope compileScope) { @@ -668,7 +690,10 @@ public class AndroidCompileUtil { } } - private static void initializeGenSourceRoot(@NotNull Module module, @Nullable String sourceRootPath, boolean createIfNotExist, boolean exclude) { + private static void initializeGenSourceRoot(@NotNull Module module, + @Nullable String sourceRootPath, + boolean createIfNotExist, + boolean exclude) { if (sourceRootPath == null) { return; } @@ -716,31 +741,31 @@ public class AndroidCompileUtil { final Resources resources = pair.getFirst(); waitForSmartMode(project); - ApplicationManager.getApplication().runReadAction(new Runnable() { - @Override - public void run() { - if (!resources.isValid() || facet.getModule().isDisposed() || project.isDisposed()) { - return; - } + ApplicationManager.getApplication().runReadAction(new Runnable() { + @Override + public void run() { + if (!resources.isValid() || facet.getModule().isDisposed() || project.isDisposed()) { + return; + } - for (final Attr attr : resources.getAttrs()) { - final String name = attr.getName().getValue(); + for (final Attr attr : resources.getAttrs()) { + final String name = attr.getName().getValue(); - if (name != null) { - resourceSet.add(new ResourceEntry(ResourceType.ATTR.getName(), name)); - } - } - - for (final DeclareStyleable styleable : resources.getDeclareStyleables()) { - final String name = styleable.getName().getValue(); - - if (name != null) { - resourceSet.add(new ResourceEntry(ResourceType.DECLARE_STYLEABLE.getName(), name)); - } + if (name != null) { + resourceSet.add(new ResourceEntry(ResourceType.ATTR.getName(), name)); } } - }); - } + + for (final DeclareStyleable styleable : resources.getDeclareStyleables()) { + final String name = styleable.getName().getValue(); + + if (name != null) { + resourceSet.add(new ResourceEntry(ResourceType.DECLARE_STYLEABLE.getName(), name)); + } + } + } + }); + } waitForSmartMode(project); @@ -929,4 +954,22 @@ public class AndroidCompileUtil { } return false; } + + @Nullable + public static String getUnsignedApkPath(@NotNull AndroidFacet facet) { + final String apkPath = AndroidRootUtil.getApkPath(facet); + return apkPath != null ? AndroidCommonUtils.addSuffixToFileName(apkPath, UNSIGNED_SUFFIX) : null; + } + + @Nullable + public static T handleExceptionError(@NotNull CompileContext context, + @NotNull String messagePrefix, + @NotNull Exception e) { + reportException(context, messagePrefix, e); + return null; + } + + public static void reportException(@NotNull CompileContext context, @NotNull String messagePrefix, @NotNull Exception e) { + context.addMessage(CompilerMessageCategory.ERROR, messagePrefix + e.getClass().getSimpleName() + ": " + e.getMessage(), null, -1, -1); + } } \ No newline at end of file diff --git a/plugins/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java b/plugins/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java index 3cf23e95fcd2..3bc201520429 100644 --- a/plugins/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java +++ b/plugins/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java @@ -16,6 +16,7 @@ package org.jetbrains.android.compiler; import com.intellij.compiler.CompilerIOUtil; +import com.intellij.compiler.impl.CompilerUtil; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.compiler.*; import com.intellij.openapi.diagnostic.Logger; @@ -27,6 +28,7 @@ import com.intellij.openapi.roots.ModuleOrderEntry; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.OrderEntry; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.JarFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.containers.HashSet; @@ -54,8 +56,6 @@ import java.util.*; public class AndroidPackagingCompiler implements PackagingCompiler { private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.compiler.AndroidPackagingCompiler"); - public static final String UNSIGNED_SUFFIX = ".unsigned"; - public void processOutdatedItem(CompileContext context, String url, @Nullable ValidityState state) { } @@ -235,7 +235,7 @@ public class AndroidPackagingCompiler implements PackagingCompiler { : item.getResPackagePath(); final String finalPath = unsigned - ? AndroidCommonUtils.addSuffixToFileName(item.getFinalPath(), UNSIGNED_SUFFIX) + ? AndroidCommonUtils.addSuffixToFileName(item.getFinalPath(), AndroidCompileUtil.UNSIGNED_SUFFIX) : item.getFinalPath(); final String[] sourceRoots = AndroidCompileUtil.toOsPaths(item.getSourceRoots()); @@ -247,6 +247,18 @@ public class AndroidPackagingCompiler implements PackagingCompiler { item.getAdditionalNativeLibs(), finalPath, unsigned, item.mySdkPath, item.getCustomKeystorePath(), new ExcludedSourcesFilter(project))); + if (messages.get(CompilerMessageCategory.ERROR).size() == 0) { + if (item.myReleaseBuild == unsigned) { + final File dst = new File( + AndroidCommonUtils.addSuffixToFileName(item.getFinalPath(), AndroidCommonUtils.ANDROID_FINAL_PACKAGE_FOR_ARTIFACT_SUFFIX)); + FileUtil.copy(new File(finalPath), dst); + CompilerUtil.refreshIOFile(dst); + final VirtualFile jar = JarFileSystem.getInstance().refreshAndFindFileByPath(dst.getPath() + "!/"); + if (jar != null) { + jar.refresh(false, true); + } + } + } AndroidCompileUtil.addMessages(context, messages, item.myModule); } catch (final IOException e) { diff --git a/plugins/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java b/plugins/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java index e4d6b8b2295f..3b952b9eb0c9 100644 --- a/plugins/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java +++ b/plugins/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java @@ -17,6 +17,8 @@ package org.jetbrains.android.compiler; import com.intellij.compiler.CompilerConfiguration; import com.intellij.compiler.CompilerConfigurationImpl; +import com.intellij.compiler.options.CompileStepBeforeRun; +import com.intellij.facet.ProjectFacetManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.compiler.*; @@ -35,7 +37,13 @@ import com.intellij.openapi.roots.OrderEntry; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.artifacts.ArtifactProperties; +import com.intellij.packaging.impl.compiler.ArtifactCompileScope; import com.intellij.util.containers.hash.HashSet; +import org.jetbrains.android.compiler.artifact.AndroidApplicationArtifactProperties; +import org.jetbrains.android.compiler.artifact.AndroidArtifactPropertiesProvider; +import org.jetbrains.android.compiler.artifact.AndroidArtifactSigningMode; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.AndroidRootUtil; import org.jetbrains.android.maven.AndroidMavenUtil; @@ -55,10 +63,13 @@ public class AndroidPrecompileTask implements CompileTask { @Override public boolean execute(CompileContext context) { + if (!checkArtifacts(context)) { + return false; + } checkAndroidDependencies(context); final Project project = context.getProject(); - + ExcludedEntriesConfiguration configuration = ((CompilerConfigurationImpl)CompilerConfiguration.getInstance(project)).getExcludedEntriesConfiguration(); @@ -107,6 +118,62 @@ public class AndroidPrecompileTask implements CompileTask { return true; } + private static boolean checkArtifacts(@NotNull CompileContext context) { + final Project project = context.getProject(); + if (!ProjectFacetManager.getInstance(project).hasFacets(AndroidFacet.ID)) { + return true; + } + + final Set artifacts = ArtifactCompileScope.getArtifactsToBuild(project, context.getCompileScope(), false); + if (artifacts == null) { + return true; + } + final Set debugArtifacts = new HashSet(); + final Set releaseArtifacts = new HashSet(); + + for (final Artifact artifact : artifacts) { + final ArtifactProperties properties = artifact.getProperties(AndroidArtifactPropertiesProvider.getInstance()); + if (properties instanceof AndroidApplicationArtifactProperties) { + final AndroidArtifactSigningMode mode = ((AndroidApplicationArtifactProperties)properties).getSigningMode(); + if (mode == AndroidArtifactSigningMode.DEBUG) { + debugArtifacts.add(artifact); + } + else { + releaseArtifacts.add(artifact); + } + } + } + boolean success = true; + + if (debugArtifacts.size() > 0 && releaseArtifacts.size() > 0) { + final String message = "Cannot build debug and release Android artifacts in the same session\n" + + "Debug artifacts: " + toString(debugArtifacts) + "\n" + + "Release artifacts: " + toString(releaseArtifacts); + context.addMessage(CompilerMessageCategory.ERROR, message, null, -1, -1); + success = false; + } + + if (releaseArtifacts.size() > 0 && + CompileStepBeforeRun.getRunConfiguration(context) != null) { + final String message = "Cannot build release Android artifacts in the 'make before run' session\n" + + "Release artifacts: " + toString(releaseArtifacts); + context.addMessage(CompilerMessageCategory.ERROR, message, null, -1, -1); + success = false; + } + return success; + } + + private static String toString(Collection artifacts) { + final StringBuilder result = new StringBuilder(); + for (Artifact artifact : artifacts) { + if (result.length() > 0) { + result.append(", "); + } + result.append(artifact.getName()); + } + return result.toString(); + } + private static void checkAndroidDependencies(@NotNull CompileContext context) { for (Module module : context.getCompileScope().getAffectedModules()) { final AndroidFacet facet = AndroidFacet.getInstance(module); @@ -142,7 +209,7 @@ public class AndroidPrecompileTask implements CompileTask { } } } - + private static void clearResCache(@NotNull AndroidFacet facet, @NotNull CompileContext context) { final Module module = facet.getModule(); diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactProperties.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactProperties.java new file mode 100644 index 000000000000..c7ee3c802239 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactProperties.java @@ -0,0 +1,250 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.openapi.compiler.CompileContext; +import com.intellij.openapi.compiler.CompilerMessageCategory; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.artifacts.ArtifactProperties; +import com.intellij.packaging.ui.ArtifactEditorContext; +import com.intellij.packaging.ui.ArtifactPropertiesEditor; +import com.intellij.util.xmlb.XmlSerializerUtil; +import com.intellij.util.xmlb.annotations.Transient; +import org.apache.commons.codec.binary.Base64; +import org.jetbrains.android.compiler.AndroidCompileUtil; +import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidCommonUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * @author Eugene.Kudelevsky + */ +@SuppressWarnings("UnusedDeclaration") +public class AndroidApplicationArtifactProperties extends ArtifactProperties { + private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.compiler.artifact.AndroidApplicationArtifactProperties"); + + private AndroidArtifactSigningMode mySigningMode = AndroidArtifactSigningMode.RELEASE_UNSIGNED; + private String myKeyStoreUrl = ""; + private String myKeyStorePassword = ""; + private String myKeyAlias = ""; + private String myKeyPassword = ""; + + @Override + public void onBuildFinished(@NotNull Artifact artifact, @NotNull CompileContext context) { + if (!(artifact.getArtifactType() instanceof AndroidApplicationArtifactType) || + mySigningMode != AndroidArtifactSigningMode.RELEASE_SIGNED) { + return; + } + final String errorPrefix = "[Artifact '" + artifact.getName() + "'] "; + final Pair pair = getPrivateKeyAndCertificate(context, errorPrefix); + if (pair == null) { + return; + } + + final String artifactFilePath = artifact.getOutputFilePath(); + final String prefix = "Cannot sign artifact " + artifact.getName() + ": "; + + if (artifactFilePath == null) { + context.addMessage(CompilerMessageCategory.ERROR, prefix + "output path is not specified", null, -1, -1); + return; + } + + final File artifactFile = new File(artifactFilePath); + if (!artifactFile.exists()) { + context.addMessage(CompilerMessageCategory.ERROR, prefix + "file " + artifactFilePath + " hasn't been generated", null, -1, -1); + return; + } + + File tmpDir = null; + try { + tmpDir = FileUtil.createTempDirectory("android_artifact", "tmp"); + final File tmpArtifact = new File(tmpDir, "tmpArtifact.apk"); + FileUtil.copy(artifactFile, tmpArtifact); + + if (!FileUtil.delete(artifactFile)) { + context.addMessage(CompilerMessageCategory.ERROR, "Cannot delete file " + artifactFile.getPath(), null, -1, -1); + return; + } + AndroidCommonUtils.signApk(tmpArtifact, artifactFile, pair.getFirst(), pair.getSecond()); + } + catch (IOException e) { + context.addMessage(CompilerMessageCategory.ERROR, prefix + "I/O error: " + e.getMessage(), null, -1, -1); + } + catch (GeneralSecurityException e) { + AndroidCompileUtil.reportException(context, prefix, e); + } + finally { + if (tmpDir != null) { + FileUtil.delete(tmpDir); + } + } + } + + @Override + public ArtifactPropertiesEditor createEditor(@NotNull ArtifactEditorContext context) { + return new AndroidArtifactPropertiesEditor(this, context.getProject()); + } + + @Override + public AndroidApplicationArtifactProperties getState() { + return this; + } + + @Override + public void loadState(AndroidApplicationArtifactProperties state) { + XmlSerializerUtil.copyBean(state, this); + } + + @Nullable + private Pair getPrivateKeyAndCertificate(@NotNull CompileContext context, @NotNull String errorPrefix) { + final String keyStoreFilePath = myKeyStoreUrl != null ? VfsUtilCore.urlToPath(myKeyStoreUrl) : ""; + + if (keyStoreFilePath.length() == 0) { + context.addMessage(CompilerMessageCategory.ERROR, errorPrefix + "Key store file is not specified", null, -1, -1); + return null; + } + if (myKeyStorePassword == null || myKeyStorePassword.length() == 0) { + context.addMessage(CompilerMessageCategory.ERROR, errorPrefix + "Key store password is not specified", null, -1, -1); + return null; + } + if (myKeyAlias == null || myKeyAlias.length() == 0) { + context.addMessage(CompilerMessageCategory.ERROR, errorPrefix + "Key alias is not specified", null, -1, -1); + return null; + } + if (myKeyPassword == null || myKeyPassword.length() == 0) { + context.addMessage(CompilerMessageCategory.ERROR, errorPrefix + "Key password is not specified", null, -1, -1); + return null; + } + final File keyStoreFile = new File(keyStoreFilePath); + final String keystorePasswordStr = getPlainKeystorePassword(); + final char[] keystorePassword = keystorePasswordStr.toCharArray(); + + final String keyPasswordStr = getPlainKeyPassword(); + final char[] keyPassword = keyPasswordStr.toCharArray(); + + final KeyStore keyStore; + InputStream is = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + is = new FileInputStream(keyStoreFile); + keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(is, keystorePassword); + + final KeyStore.PrivateKeyEntry entry = + (KeyStore.PrivateKeyEntry)keyStore.getEntry(myKeyAlias, new KeyStore.PasswordProtection(keyPassword)); + if (entry == null) { + context.addMessage(CompilerMessageCategory.ERROR, + errorPrefix + AndroidBundle.message("android.extract.package.cannot.find.key.error", myKeyAlias), null, -1, -1); + return null; + } + + final PrivateKey privateKey = entry.getPrivateKey(); + final Certificate certificate = entry.getCertificate(); + if (privateKey == null || certificate == null) { + context.addMessage(CompilerMessageCategory.ERROR, + errorPrefix + AndroidBundle.message("android.extract.package.cannot.find.key.error", myKeyAlias), null, -1, -1); + return null; + } + return Pair.create(privateKey, (X509Certificate)certificate); + } + catch (FileNotFoundException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + catch (KeyStoreException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + catch (CertificateException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + catch (NoSuchAlgorithmException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + catch (IOException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + catch (UnrecoverableEntryException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + finally { + if (is != null) { + try { + is.close(); + } + catch (IOException e) { + LOG.info(e); + } + } + } + } + + public AndroidArtifactSigningMode getSigningMode() { + return mySigningMode; + } + + public void setSigningMode(AndroidArtifactSigningMode signingMode) { + mySigningMode = signingMode; + } + + public String getKeyStoreUrl() { + return myKeyStoreUrl; + } + + public String getKeyStorePassword() { + return myKeyStorePassword; + } + + public String getKeyAlias() { + return myKeyAlias; + } + + public String getKeyPassword() { + return myKeyPassword; + } + + public void setKeyStoreUrl(String keyStoreUrl) { + myKeyStoreUrl = keyStoreUrl; + } + + public void setKeyStorePassword(String keyStorePassword) { + myKeyStorePassword = keyStorePassword; + } + + public void setKeyAlias(String keyAlias) { + myKeyAlias = keyAlias; + } + + public void setKeyPassword(String keyPassword) { + myKeyPassword = keyPassword; + } + + @Transient + @NotNull + public String getPlainKeystorePassword() { + return new String(new Base64().decode(myKeyStorePassword.getBytes())); + } + + @Transient + public void setPlainKeystorePassword(@NotNull String password) { + myKeyStorePassword = new String(new Base64().encode(password.getBytes())); + } + + @Transient + @NotNull + public String getPlainKeyPassword() { + return new String(new Base64().decode(myKeyPassword.getBytes())); + } + + @Transient + public void setPlainKeyPassword(@NotNull String password) { + myKeyPassword = new String(new Base64().encode(password.getBytes())); + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactType.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactType.java new file mode 100644 index 000000000000..ea268a3fac13 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactType.java @@ -0,0 +1,95 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.roots.ui.configuration.artifacts.sourceItems.LibrarySourceItem; +import com.intellij.openapi.roots.ui.configuration.artifacts.sourceItems.ModuleOutputSourceItem; +import com.intellij.packaging.artifacts.ArtifactTemplate; +import com.intellij.packaging.artifacts.ArtifactType; +import com.intellij.packaging.elements.CompositePackagingElement; +import com.intellij.packaging.elements.PackagingElementFactory; +import com.intellij.packaging.elements.PackagingElementOutputKind; +import com.intellij.packaging.elements.PackagingElementResolvingContext; +import com.intellij.packaging.impl.artifacts.ArtifactUtil; +import com.intellij.packaging.impl.artifacts.PlainArtifactType; +import com.intellij.packaging.ui.PackagingSourceItem; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidApplicationArtifactType extends ArtifactType { + public AndroidApplicationArtifactType() { + super("apk", "Android Application"); + } + + @NotNull + @Override + public Icon getIcon() { + return PlainArtifactType.ARTIFACT_ICON; + } + + @Override + public String getDefaultPathFor(@NotNull PackagingElementOutputKind kind) { + return "/"; + } + + @Override + public boolean isSuitableItem(@NotNull PackagingSourceItem item) { + return !(item instanceof ModuleOutputSourceItem || item instanceof LibrarySourceItem); + } + + @NotNull + @Override + public CompositePackagingElement createRootElement(@NotNull String artifactName) { + return PackagingElementFactory.getInstance().createArchive(ArtifactUtil.suggestArtifactFileName(artifactName) + ".apk"); + } + + + @NotNull + @Override + public List getNewArtifactTemplates(@NotNull PackagingElementResolvingContext context) { + return Collections.singletonList(new MyTemplate(context)); + } + + private class MyTemplate extends ArtifactTemplate { + protected PackagingElementResolvingContext myContext; + + public MyTemplate(@NotNull PackagingElementResolvingContext context) { + myContext = context; + } + + @Override + public String getPresentableName() { + return "From module..."; + } + + @Override + public NewArtifactConfiguration createArtifact() { + final List modules = new ArrayList(); + + for (Module module : myContext.getModulesProvider().getModules()) { + final AndroidFacet facet = AndroidFacet.getInstance(module); + + if (facet != null && !facet.getConfiguration().LIBRARY_PROJECT) { + modules.add(module); + } + } + + final AndroidFacet facet = AndroidArtifactUtil.chooseAndroidApplicationModule(myContext.getProject(), modules); + if (facet == null) { + return null; + } + + final CompositePackagingElement rootElement = + AndroidApplicationArtifactType.this.createRootElement(facet.getModule().getName()); + rootElement.addFirstChild(new AndroidFinalPackageElement(myContext.getProject(), facet)); + return new NewArtifactConfiguration(rootElement, facet.getModule().getName(), AndroidApplicationArtifactType.this); + } + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.form b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.form new file mode 100644 index 000000000000..cef9f344b696 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.form @@ -0,0 +1,157 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.java new file mode 100644 index 000000000000..8c86c94895ef --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.java @@ -0,0 +1,260 @@ +package org.jetbrains.android.compiler.artifact; + +import com.android.annotations.NonNull; +import com.intellij.CommonBundle; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.fileChooser.FileChooser; +import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.packaging.ui.ArtifactPropertiesEditor; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.*; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidArtifactPropertiesEditor extends ArtifactPropertiesEditor { + private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.compiler.artifact.AndroidApplicationArtifactProperties"); + + private final AndroidApplicationArtifactProperties myProperties; + + private JPanel myPanel; + private JRadioButton myDebugRadio; + private JRadioButton myReleaseSignedRadio; + private JRadioButton myReleaseUnsignedRadio; + private JPanel myReleaseKeyPanel; + private JPasswordField myKeyStorePasswordField; + private JTextField myKeyStorePathField; + private JPasswordField myKeyPasswordField; + private TextFieldWithBrowseButton myKeyAliasField; + private JButton myLoadKeyStoreButton; + private JButton myCreateKeyStoreButton; + + public AndroidArtifactPropertiesEditor(@NonNull AndroidApplicationArtifactProperties properties, @NotNull final Project project) { + myProperties = properties; + + final ActionListener listener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + UIUtil.setEnabled(myReleaseKeyPanel, myReleaseSignedRadio.isSelected(), true); + } + }; + myDebugRadio.addActionListener(listener); + myReleaseUnsignedRadio.addActionListener(listener); + myReleaseSignedRadio.addActionListener(listener); + + myLoadKeyStoreButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final String defaultPath = getKeyStorePath(); + final VirtualFile defaultFile = LocalFileSystem.getInstance().findFileByPath(defaultPath); + final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor(); + final VirtualFile file = FileChooser.chooseFile(descriptor, myPanel, project, defaultFile); + if (file != null) { + myKeyStorePathField.setText(FileUtil.toSystemDependentName(file.getPath())); + } + } + }); + + myCreateKeyStoreButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final NewKeyStoreDialog dialog = new NewKeyStoreDialog(project, myKeyStorePathField.getText()); + dialog.show(); + + if (dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) { + myKeyStorePathField.setText(dialog.getKeyStorePath()); + myKeyStorePasswordField.setText(String.valueOf(dialog.getKeyStorePassword())); + myKeyAliasField.setText(dialog.getKeyAlias()); + myKeyPasswordField.setText(String.valueOf(dialog.getKeyPassword())); + } + } + }); + + myKeyAliasField.getButton().addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final List keys = loadExistingKeys(); + if (keys == null) { + return; + } + final ChooseKeyDialog dialog = + new ChooseKeyDialog(project, getKeyStorePath(), myKeyStorePasswordField.getPassword(), keys, getKeyAlias()); + dialog.show(); + + if (dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) { + final String chosenKey = dialog.getChosenKey(); + if (chosenKey != null) { + myKeyAliasField.setText(chosenKey); + } + + final char[] password = dialog.getChosenKeyPassword(); + if (password != null) { + myKeyPasswordField.setText(String.valueOf(password)); + } + } + } + }); + } + + private String getKeyStorePath() { + return myKeyStorePathField.getText().trim(); + } + + @Nullable + private List loadExistingKeys() { + final String errorPrefix = "Cannot load key store: "; + InputStream is = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + is = new FileInputStream(new File(getKeyStorePath())); + final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(is, myKeyStorePasswordField.getPassword()); + return AndroidUtils.toList(keyStore.aliases()); + } + catch (KeyStoreException e) { + Messages.showErrorDialog(myPanel, errorPrefix + e.getMessage(), CommonBundle.getErrorTitle()); + return null; + } + catch (FileNotFoundException e) { + Messages.showErrorDialog(myPanel, errorPrefix + e.getMessage(), CommonBundle.getErrorTitle()); + return null; + } + catch (CertificateException e) { + Messages.showErrorDialog(myPanel, errorPrefix + e.getMessage(), CommonBundle.getErrorTitle()); + return null; + } + catch (NoSuchAlgorithmException e) { + Messages.showErrorDialog(myPanel, errorPrefix + e.getMessage(), CommonBundle.getErrorTitle()); + return null; + } + catch (IOException e) { + Messages.showErrorDialog(myPanel, errorPrefix + e.getMessage(), CommonBundle.getErrorTitle()); + return null; + } + finally { + if (is != null) { + try { + is.close(); + } + catch (IOException e) { + LOG.info(e); + } + } + } + } + + @Override + public String getTabName() { + return "Android"; + } + + @Override + public JComponent createComponent() { + return myPanel; + } + + @Override + public boolean isModified() { + return getSigningMode() != myProperties.getSigningMode() || + !getKeyStoreFileUrl().equals(myProperties.getKeyStoreUrl()) || + !getKeyStorePassword().equals(myProperties.getPlainKeystorePassword()) || + !getKeyAlias().equals(myProperties.getKeyAlias()) || + !getKeyPassword().equals(myProperties.getPlainKeyPassword()); + } + + @Override + public void apply() { + myProperties.setSigningMode(getSigningMode()); + myProperties.setKeyStoreUrl(getKeyStoreFileUrl()); + myProperties.setPlainKeystorePassword(getKeyStorePassword()); + myProperties.setKeyAlias(getKeyAlias()); + myProperties.setPlainKeyPassword(getKeyPassword()); + } + + @Override + public void reset() { + switch (myProperties.getSigningMode()) { + case RELEASE_UNSIGNED: + myReleaseUnsignedRadio.setSelected(true); + myDebugRadio.setSelected(false); + myReleaseSignedRadio.setSelected(false); + break; + case DEBUG: + myReleaseUnsignedRadio.setSelected(false); + myDebugRadio.setSelected(true); + myReleaseSignedRadio.setSelected(false); + break; + case RELEASE_SIGNED: + myReleaseUnsignedRadio.setSelected(false); + myDebugRadio.setSelected(false); + myReleaseSignedRadio.setSelected(true); + break; + } + final String keyStoreUrl = myProperties.getKeyStoreUrl(); + myKeyStorePathField.setText(keyStoreUrl != null ? VfsUtilCore.urlToPath(keyStoreUrl) : ""); + myKeyStorePasswordField.setText(myProperties.getPlainKeystorePassword()); + + final String keyAlias = myProperties.getKeyAlias(); + myKeyAliasField.setText(keyAlias != null ? keyAlias : ""); + myKeyPasswordField.setText(myProperties.getPlainKeyPassword()); + + UIUtil.setEnabled(myReleaseKeyPanel, myProperties.getSigningMode() == AndroidArtifactSigningMode.RELEASE_SIGNED, true); + } + + @Override + public void disposeUIResources() { + } + + @NotNull + private AndroidArtifactSigningMode getSigningMode() { + if (myDebugRadio.isSelected()) { + return AndroidArtifactSigningMode.DEBUG; + } + else if (myReleaseSignedRadio.isSelected()) { + return AndroidArtifactSigningMode.RELEASE_SIGNED; + } + return AndroidArtifactSigningMode.RELEASE_UNSIGNED; + } + + @NotNull + private String getKeyStoreFileUrl() { + final String path = getKeyStorePath(); + return VfsUtilCore.pathToUrl(path); + } + + @NotNull + private String getKeyStorePassword() { + return String.valueOf(myKeyStorePasswordField.getPassword()); + } + + @NotNull + private String getKeyAlias() { + return myKeyAliasField.getText().trim(); + } + + @NotNull + private String getKeyPassword() { + return String.valueOf(myKeyPasswordField.getPassword()); + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesProvider.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesProvider.java new file mode 100644 index 000000000000..84161bd24df5 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesProvider.java @@ -0,0 +1,30 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.packaging.artifacts.ArtifactProperties; +import com.intellij.packaging.artifacts.ArtifactPropertiesProvider; +import com.intellij.packaging.artifacts.ArtifactType; +import org.jetbrains.annotations.NotNull; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidArtifactPropertiesProvider extends ArtifactPropertiesProvider { + protected AndroidArtifactPropertiesProvider() { + super("android-properties"); + } + + @Override + public boolean isAvailableFor(@NotNull ArtifactType type) { + return type instanceof AndroidApplicationArtifactType; + } + + @NotNull + @Override + public ArtifactProperties createProperties(@NotNull ArtifactType artifactType) { + return new AndroidApplicationArtifactProperties(); + } + + public static AndroidArtifactPropertiesProvider getInstance() { + return EP_NAME.findExtension(AndroidArtifactPropertiesProvider.class); + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningCompiler.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningCompiler.java new file mode 100644 index 000000000000..bacf8b670933 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningCompiler.java @@ -0,0 +1,122 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.compiler.CompileContext; +import com.intellij.openapi.compiler.CompileScope; +import com.intellij.openapi.compiler.PackagingCompiler; +import com.intellij.openapi.compiler.ValidityState; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidArtifactSigningCompiler implements PackagingCompiler { + @Override + public void processOutdatedItem(CompileContext context, String url, @Nullable ValidityState state) { + } + + @NotNull + @Override + public ProcessingItem[] getProcessingItems(CompileContext context) { + return ApplicationManager.getApplication().runReadAction(new Computable() { + @Override + public ProcessingItem[] compute() { + return null; + } + }); + } + + @Override + public ProcessingItem[] process(CompileContext context, ProcessingItem[] items) { + return new ProcessingItem[0]; //To change body of implemented methods use File | Settings | File Templates. + } + + @NotNull + @Override + public String getDescription() { + return "Android Artifact Signing Compiler"; + } + + @Override + public boolean validateConfiguration(CompileScope scope) { + return true; + } + + @Override + public ValidityState createValidityState(DataInput in) throws IOException { + return new MyValidityState(in); + } + + private static class MyProcessingItem implements ProcessingItem { + private final VirtualFile myApkFile; + private final AndroidArtifactSigningMode mySigningMode; + private final String myDebugKeyStorePath; + private final MyValidityState myValidityState; + + private MyProcessingItem(@NotNull VirtualFile apkFile, + @NotNull AndroidArtifactSigningMode signingMode, + @Nullable String debugKeyStorePath) { + myApkFile = apkFile; + mySigningMode = signingMode; + myDebugKeyStorePath = debugKeyStorePath; + + myValidityState = new MyValidityState(myApkFile.getModificationStamp(), + mySigningMode.name(), + myDebugKeyStorePath); + } + + @NotNull + @Override + public VirtualFile getFile() { + return myApkFile; + } + + @Override + public ValidityState getValidityState() { + return myValidityState; + } + } + + private static class MyValidityState implements ValidityState { + private final long myApkFileTimestamp; + private final String mySigningMode; + private final String myDebugKeyStorePath; + + private MyValidityState(long apkFileTimestamp, @NotNull String signingMode, @NotNull String debugKeyStorePath) { + myApkFileTimestamp = apkFileTimestamp; + mySigningMode = signingMode; + myDebugKeyStorePath = debugKeyStorePath; + } + + private MyValidityState(@NotNull DataInput in) throws IOException { + myApkFileTimestamp = in.readLong(); + mySigningMode = in.readUTF(); + myDebugKeyStorePath = in.readUTF(); + } + + @Override + public boolean equalsTo(ValidityState otherState) { + if (!(otherState instanceof MyValidityState)) { + return false; + } + final MyValidityState state = (MyValidityState)otherState; + return state.myApkFileTimestamp == myApkFileTimestamp && + state.mySigningMode.equals(mySigningMode) && + state.myDebugKeyStorePath.equals(myDebugKeyStorePath); + } + + @Override + public void save(DataOutput out) throws IOException { + out.writeLong(myApkFileTimestamp); + out.writeUTF(mySigningMode); + out.writeUTF(myDebugKeyStorePath); + } + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningMode.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningMode.java new file mode 100644 index 000000000000..4499d7a60d4d --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningMode.java @@ -0,0 +1,8 @@ +package org.jetbrains.android.compiler.artifact; + +/** + * @author Eugene.Kudelevsky + */ +public enum AndroidArtifactSigningMode { + DEBUG, RELEASE_UNSIGNED, RELEASE_SIGNED +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactUtil.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactUtil.java new file mode 100644 index 000000000000..85bf6c281842 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactUtil.java @@ -0,0 +1,56 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.CommonBundle; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ui.configuration.ChooseModulesDialog; +import com.intellij.openapi.ui.Messages; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.impl.artifacts.ArtifactUtil; +import com.intellij.packaging.ui.ArtifactEditorContext; +import com.intellij.util.Processor; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidArtifactUtil { + private AndroidArtifactUtil() { + } + + public static boolean containsAndroidPackage(ArtifactEditorContext editorContext, Artifact artifact) { + return !ArtifactUtil + .processPackagingElements(artifact, AndroidFinalPackageElementType.getInstance(), new Processor() { + public boolean process(AndroidFinalPackageElement e) { + return false; + } + }, editorContext, true); + } + + @Nullable + public static AndroidFacet chooseAndroidApplicationModule(@NotNull Project project, @NotNull List modules) { + final ChooseModulesDialog dialog = new ChooseModulesDialog(project, modules, "Select Module", + "Selected Android application module will be included in the created artifact with all dependencies"); + dialog.setSingleSelectionMode(); + dialog.show(); + final List selected = dialog.getChosenElements(); + if (selected.isEmpty()) { + return null; + } + assert selected.size() == 1; + final Module module = selected.get(0); + final String moduleName = module.getName(); + + final AndroidFacet facet = AndroidFacet.getInstance(module); + if (facet == null) { + final String message = "Cannot find Android facet for module " + moduleName; + Messages.showErrorDialog(project, message, CommonBundle.getErrorTitle()); + return null; + } + return facet; + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElement.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElement.java new file mode 100644 index 000000000000..3c719ed56d90 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElement.java @@ -0,0 +1,154 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.compiler.ant.Generator; +import com.intellij.facet.pointers.FacetPointer; +import com.intellij.facet.pointers.FacetPointersManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.JarFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.packaging.artifacts.ArtifactType; +import com.intellij.packaging.elements.*; +import com.intellij.packaging.impl.elements.FacetBasedPackagingElement; +import com.intellij.packaging.impl.elements.ModuleOutputPackagingElement; +import com.intellij.packaging.impl.ui.DelegatedPackagingElementPresentation; +import com.intellij.packaging.ui.ArtifactEditorContext; +import com.intellij.packaging.ui.PackagingElementPresentation; +import com.intellij.util.xmlb.annotations.Attribute; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.android.facet.AndroidRootUtil; +import org.jetbrains.android.util.AndroidCommonUtils; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidFinalPackageElement extends PackagingElement + implements FacetBasedPackagingElement, ModuleOutputPackagingElement { + + @NonNls static final String FACET_ATTRIBUTE = "facet"; + + private FacetPointer myFacetPointer; + private final Project myProject; + + public AndroidFinalPackageElement(@NotNull Project project, @Nullable AndroidFacet facet) { + super(AndroidFinalPackageElementType.getInstance()); + myProject = project; + myFacetPointer = facet != null ? FacetPointersManager.getInstance(myProject).create(facet) : null; + } + + @Override + public PackagingElementPresentation createPresentation(@NotNull ArtifactEditorContext context) { + return new DelegatedPackagingElementPresentation(new AndroidFinalPackagePresentation(myFacetPointer)); + } + + @Nullable + private String getApkPath() { + if (myFacetPointer == null) { + return null; + } + + final AndroidFacet facet = myFacetPointer.getFacet(); + if (facet == null) { + return null; + } + + final String apkPath = AndroidRootUtil.getApkPath(facet); + final String path = apkPath != null + ? AndroidCommonUtils.addSuffixToFileName(apkPath, AndroidCommonUtils.ANDROID_FINAL_PACKAGE_FOR_ARTIFACT_SUFFIX) + : null; + return path != null + ? FileUtil.toSystemIndependentName(path) + "!/" + : null; + } + + @Override + public List computeAntInstructions(@NotNull PackagingElementResolvingContext resolvingContext, + @NotNull AntCopyInstructionCreator creator, + @NotNull ArtifactAntGenerationContext generationContext, + @NotNull ArtifactType artifactType) { + final String apkPath = getApkPath(); + if (apkPath != null) { + return Collections.singletonList(creator.createExtractedDirectoryInstruction(apkPath)); + } + return Collections.emptyList(); + } + + @Override + public void computeIncrementalCompilerInstructions(@NotNull IncrementalCompilerInstructionCreator creator, + @NotNull PackagingElementResolvingContext resolvingContext, + @NotNull ArtifactIncrementalCompilerContext compilerContext, + @NotNull ArtifactType artifactType) { + final String apkPath = getApkPath(); + if (apkPath != null) { + final VirtualFile apk = JarFileSystem.getInstance().findFileByPath(apkPath); + if (apk != null && apk.isValid() && apk.isDirectory()) { + creator.addDirectoryCopyInstructions(apk); + } + } + } + + @Nullable + public AndroidFacet getFacet() { + return myFacetPointer != null ? myFacetPointer.getFacet() : null; + } + + @Override + public boolean isEqualTo(@NotNull PackagingElement element) { + if (!(element instanceof AndroidFinalPackageElement)) { + return false; + } + final AndroidFinalPackageElement packageElement = (AndroidFinalPackageElement)element; + + return myFacetPointer == null + ? packageElement.myFacetPointer == null + : myFacetPointer.equals(packageElement.myFacetPointer); + } + + public AndroidFinalPackageElementState getState() { + final AndroidFinalPackageElementState state = new AndroidFinalPackageElementState(); + state.myFacetPointer = myFacetPointer != null ? myFacetPointer.getId() : null; + return state; + } + + @Override + public AndroidFacet findFacet(@NotNull PackagingElementResolvingContext context) { + return myFacetPointer != null ? myFacetPointer.findFacet(context.getModulesProvider(), context.getFacetsProvider()) : null; + } + + public void loadState(AndroidFinalPackageElementState state) { + myFacetPointer = state.myFacetPointer != null + ? FacetPointersManager.getInstance(myProject).create(state.myFacetPointer) + : null; + } + + @Override + public String getModuleName() { + return myFacetPointer != null ? myFacetPointer.getModuleName() : null; + } + + @Override + public Module findModule(PackagingElementResolvingContext context) { + final AndroidFacet facet = findFacet(context); + return facet != null ? facet.getModule() : null; + } + + @NotNull + @Override + public Collection getSourceRoots(PackagingElementResolvingContext context) { + return Collections.emptyList(); + } + + public static class AndroidFinalPackageElementState { + + @Attribute(FACET_ATTRIBUTE) + public String myFacetPointer; + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElementType.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElementType.java new file mode 100644 index 000000000000..e1e76d027d5f --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElementType.java @@ -0,0 +1,88 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.elements.CompositePackagingElement; +import com.intellij.packaging.elements.PackagingElement; +import com.intellij.packaging.elements.PackagingElementType; +import com.intellij.packaging.ui.ArtifactEditorContext; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidFinalPackageElementType extends PackagingElementType { + @NonNls public static final String TYPE_ID = "android-final-package"; + + protected AndroidFinalPackageElementType() { + super(TYPE_ID, "Android Final Package"); + } + + public static AndroidFinalPackageElementType getInstance() { + return getInstance(AndroidFinalPackageElementType.class); + } + + @Override + public Icon getCreateElementIcon() { + return AndroidUtils.ANDROID_ICON; + } + + @Override + public boolean canCreate(@NotNull ArtifactEditorContext context, @NotNull Artifact artifact) { + return getAndroidApplicationFacets(context, context.getModulesProvider().getModules()).size() > 0 && + !AndroidArtifactUtil.containsAndroidPackage(context, artifact); + } + + @NotNull + private static List getAndroidApplicationFacets(@NotNull ArtifactEditorContext context, @NotNull Module[] modules) { + final List result = new ArrayList(); + for (Module module : modules) { + for (AndroidFacet facet : context.getFacetsProvider().getFacetsByType(module, AndroidFacet.ID)) { + if (!facet.getConfiguration().LIBRARY_PROJECT) { + result.add(facet); + } + } + } + return result; + } + + @NotNull + private static List facetsToModules(@NotNull Collection facets) { + final List result = new ArrayList(facets.size()); + for (AndroidFacet facet : facets) { + result.add(facet.getModule()); + } + return result; + } + + @NotNull + @Override + public List> chooseAndCreate(@NotNull ArtifactEditorContext context, + @NotNull Artifact artifact, + @NotNull CompositePackagingElement parent) { + final List facets = getAndroidApplicationFacets(context, context.getModulesProvider().getModules()); + final List modules = facetsToModules(facets); + + final AndroidFacet facet = AndroidArtifactUtil.chooseAndroidApplicationModule(context.getProject(), modules); + if (facet == null) { + return Collections.emptyList(); + } + return Collections.singletonList(new AndroidFinalPackageElement(context.getProject(), facet)); + } + + @NotNull + @Override + public AndroidFinalPackageElement createEmpty(@NotNull Project project) { + return new AndroidFinalPackageElement(project, null); + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackagePresentation.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackagePresentation.java new file mode 100644 index 000000000000..d43b9cb2c08b --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackagePresentation.java @@ -0,0 +1,40 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.facet.pointers.FacetPointer; +import com.intellij.ide.projectView.PresentationData; +import com.intellij.packaging.ui.SourceItemPresentation; +import com.intellij.packaging.ui.SourceItemWeights; +import com.intellij.ui.SimpleTextAttributes; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Eugene.Kudelevsky + */ +class AndroidFinalPackagePresentation extends SourceItemPresentation { + private final FacetPointer myFacetPointer; + + public AndroidFinalPackagePresentation(@Nullable FacetPointer facetPointer) { + myFacetPointer = facetPointer; + } + + @Override + public String getPresentableName() { + final String moduleName = myFacetPointer != null ? myFacetPointer.getModuleName() : ""; + return "'" + moduleName + "' Android final package"; + } + + @Override + public void render(@NotNull PresentationData presentationData, + SimpleTextAttributes mainAttributes, + SimpleTextAttributes commentAttributes) { + presentationData.setIcons(AndroidFacet.getFacetType().getIcon()); + presentationData.addText(getPresentableName(), mainAttributes); + } + + @Override + public int getWeight() { + return SourceItemWeights.LIBRARY_WEIGHT - 5; + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidSourceItemsProvider.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidSourceItemsProvider.java new file mode 100644 index 000000000000..4c2528464ed6 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidSourceItemsProvider.java @@ -0,0 +1,56 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.facet.pointers.FacetPointersManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.roots.ui.configuration.artifacts.sourceItems.FacetBasedPackagingSourceItemsProvider; +import com.intellij.openapi.roots.ui.configuration.artifacts.sourceItems.ModuleSourceItemGroup; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.ui.ArtifactEditorContext; +import com.intellij.packaging.ui.PackagingSourceItem; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidSourceItemsProvider extends FacetBasedPackagingSourceItemsProvider { + public AndroidSourceItemsProvider() { + super(AndroidFacet.ID, AndroidFinalPackageElementType.getInstance()); + } + + @Override + protected AndroidFinalPackagePresentation createPresentation(AndroidFacet facet) { + return new AndroidFinalPackagePresentation(FacetPointersManager.getInstance(facet.getModule().getProject()).create(facet)); + } + + @Override + protected AndroidFinalPackageElement createElement(ArtifactEditorContext context, AndroidFacet facet) { + return new AndroidFinalPackageElement(context.getProject(), facet); + } + + @NotNull + @Override + public Collection getSourceItems(@NotNull ArtifactEditorContext editorContext, + @NotNull Artifact artifact, + @Nullable PackagingSourceItem parent) { + + if (parent instanceof ModuleSourceItemGroup && !AndroidArtifactUtil.containsAndroidPackage(editorContext, artifact)) { + final Module module = ((ModuleSourceItemGroup)parent).getModule(); + final Set facets = + new HashSet(editorContext.getFacetsProvider().getFacetsByType(module, AndroidFacet.ID)); + return Collections.singletonList(new FacetBasedSourceItem(this, facets.iterator().next())); + } + return Collections.emptyList(); + } + + @Override + protected AndroidFacet getFacet(AndroidFinalPackageElement element) { + return element.getFacet(); + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.form b/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.form new file mode 100644 index 000000000000..0b5728b4ae74 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.form @@ -0,0 +1,57 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.java new file mode 100644 index 000000000000..7b0695955515 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.java @@ -0,0 +1,129 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.CommonBundle; +import com.intellij.ide.wizard.CommitStepException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.ui.CollectionComboBoxModel; +import com.intellij.ui.components.JBRadioButton; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.android.exportSignedPackage.NewKeyForm; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class ChooseKeyDialog extends DialogWrapper { + private JPanel myNewKeyPanel; + private JBRadioButton myCreateNewKeyRadioButton; + private JBRadioButton myUseExistingKeyRadioButton; + private JComboBox myKeyCombo; + private JPanel myPanel; + + private final NewKeyForm myNewKeyForm = new MyNewKeyForm(); + private final Project myProject; + private final String myKeyStorePath; + private final char[] myKeyStorePassword; + private final List myExistingKeys; + + public ChooseKeyDialog(@NotNull Project project, + @NotNull String keyStorePath, + @NotNull char[] password, + @NotNull List existingKeys, + @Nullable String keyToSelect) { + super(project); + myProject = project; + myKeyStorePath = keyStorePath; + myKeyStorePassword = password; + myExistingKeys = existingKeys; + myKeyCombo.setModel(new CollectionComboBoxModel(existingKeys, existingKeys.get(0))); + + if (keyToSelect != null && existingKeys.contains(keyToSelect)) { + myKeyCombo.setSelectedItem(keyToSelect); + } + myNewKeyPanel.add(myNewKeyForm.getContentPanel(), BorderLayout.CENTER); + + final ActionListener listener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + UIUtil.setEnabled(myNewKeyPanel, myCreateNewKeyRadioButton.isSelected(), true); + } + }; + myCreateNewKeyRadioButton.addActionListener(listener); + myUseExistingKeyRadioButton.addActionListener(listener); + + final boolean useExisting = existingKeys.size() > 0; + myUseExistingKeyRadioButton.setSelected(useExisting); + myCreateNewKeyRadioButton.setSelected(!useExisting); + UIUtil.setEnabled(myNewKeyPanel, !useExisting, true); + + setTitle("Create New Key"); + init(); + } + + @Override + protected JComponent createCenterPanel() { + return myPanel; + } + + @Override + protected void doOKAction() { + if (myCreateNewKeyRadioButton.isSelected()) { + try { + myNewKeyForm.createKey(); + } + catch (CommitStepException e) { + Messages.showErrorDialog(myPanel, e.getMessage(), CommonBundle.getErrorTitle()); + return; + } + } + super.doOKAction(); + } + + @Nullable + public String getChosenKey() { + return myUseExistingKeyRadioButton.isSelected() + ? (String)myKeyCombo.getSelectedItem() + : myNewKeyForm.getKeyAlias(); + } + + @Nullable + public char[] getChosenKeyPassword() { + return myCreateNewKeyRadioButton.isSelected() + ? myNewKeyForm.getKeyPassword() + : null; + } + + private class MyNewKeyForm extends NewKeyForm { + @Override + protected List getExistingKeyAliasList() { + return myExistingKeys; + } + + @NotNull + @Override + protected Project getProject() { + return myProject; + } + + @NotNull + @Override + protected char[] getKeyStorePassword() { + return myKeyStorePassword; + } + + @NotNull + @Override + protected String getKeyStoreLocation() { + return myKeyStorePath; + } + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.form b/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.form new file mode 100644 index 000000000000..ade0f50564b1 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.form @@ -0,0 +1,78 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.java new file mode 100644 index 000000000000..7ea7ae3a88be --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.java @@ -0,0 +1,124 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.CommonBundle; +import com.intellij.ide.wizard.CommitStepException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import org.jetbrains.android.exportSignedPackage.NewKeyForm; +import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.android.util.SaveFileListener; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.util.Collections; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class NewKeyStoreDialog extends DialogWrapper { + private JPanel myNewKeyPanel; + private JPanel myPanel; + private TextFieldWithBrowseButton myKeyStorePathField; + private JPasswordField myPasswordField; + private JPasswordField myConfirmedPassword; + + private final NewKeyForm myNewKeyForm; + private final Project myProject; + + public NewKeyStoreDialog(@NotNull Project project, @NotNull String defaultKeyStorePath) { + super(project); + myProject = project; + myKeyStorePathField.setText(defaultKeyStorePath); + setTitle("Create New Key Store"); + myNewKeyForm = new MyNewKeyForm(); + myNewKeyPanel.add(myNewKeyForm.getContentPanel(), BorderLayout.CENTER); + + myKeyStorePathField.addActionListener(new SaveFileListener(myPanel, myKeyStorePathField, AndroidBundle.message( + "android.extract.package.choose.keystore.title")) { + @Override + protected String getDefaultLocation() { + return getKeyStorePath(); + } + }); + init(); + } + + @Override + protected JComponent createCenterPanel() { + return myPanel; + } + + @Override + public JComponent getPreferredFocusedComponent() { + return myKeyStorePathField; + } + + @Override + protected void doOKAction() { + if (getKeyStorePath().length() == 0) { + Messages.showErrorDialog(myPanel, "Specify key store path", CommonBundle.getErrorTitle()); + return; + } + + try { + AndroidUtils.checkNewPassword(myPasswordField, myConfirmedPassword); + myNewKeyForm.createKey(); + } + catch (CommitStepException e) { + Messages.showErrorDialog(myPanel, e.getMessage(), CommonBundle.getErrorTitle()); + return; + } + super.doOKAction(); + } + + @NotNull + public String getKeyStorePath() { + return myKeyStorePathField.getText().trim(); + } + + @NotNull + public char[] getKeyStorePassword() { + return myPasswordField.getPassword(); + } + + @NotNull + public String getKeyAlias() { + return myNewKeyForm.getKeyAlias(); + } + + @NotNull + public char[] getKeyPassword() { + return myNewKeyForm.getKeyPassword(); + } + + private class MyNewKeyForm extends NewKeyForm { + + @Override + protected List getExistingKeyAliasList() { + return Collections.emptyList(); + } + + @NotNull + @Override + protected Project getProject() { + return myProject; + } + + @NotNull + @Override + protected char[] getKeyStorePassword() { + return NewKeyStoreDialog.this.getKeyStorePassword(); + } + + @NotNull + @Override + protected String getKeyStoreLocation() { + return getKeyStorePath(); + } + } +} diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/ApkStep.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/ApkStep.java index 06547b1bd90b..6c25426a7b9e 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/ApkStep.java +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/ApkStep.java @@ -16,7 +16,6 @@ package org.jetbrains.android.exportSignedPackage; -import com.android.jarutils.SignedJarBuilder; import com.android.sdklib.SdkConstants; import com.intellij.CommonBundle; import com.intellij.execution.ExecutionException; @@ -50,7 +49,6 @@ import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.components.JBLabel; import org.jetbrains.android.compiler.AndroidCompileUtil; -import org.jetbrains.android.compiler.AndroidPackagingCompiler; import org.jetbrains.android.compiler.AndroidProguardCompiler; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.AndroidFacetConfiguration; @@ -58,7 +56,6 @@ import org.jetbrains.android.facet.AndroidRootUtil; import org.jetbrains.android.sdk.AndroidPlatform; import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.android.util.AndroidCommonUtils; -import org.jetbrains.android.util.SafeSignedJarBuilder; import org.jetbrains.android.util.SaveFileListener; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -68,12 +65,8 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; /** * @author Eugene.Kudelevsky @@ -110,7 +103,7 @@ class ApkStep extends ExportSignedPackageWizardStep { myWizard = wizard; myApkPathLabel.setLabelFor(myApkPathField); myProguardConfigFilePathLabel.setLabelFor(myProguardConfigFilePathField); - + myApkPathField.getButton().addActionListener( new SaveFileListener(myContentPanel, myApkPathField, AndroidBundle.message("android.extract.package.choose.dest.apk")) { @Override @@ -140,7 +133,7 @@ class ApkStep extends ExportSignedPackageWizardStep { } } }); - + myProguardCheckBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -193,7 +186,7 @@ class ApkStep extends ExportSignedPackageWizardStep { myIncludeSystemProguardFileCheckBox.setVisible(AndroidCommonUtils.isIncludingInProguardSupported(sdkToolsRevision)); final String proguardCfgPath = properties.getValue(PROGUARD_CFG_PATH_PROPERTY); - if (proguardCfgPath != null && + if (proguardCfgPath != null && LocalFileSystem.getInstance().refreshAndFindFileByPath(proguardCfgPath) != null) { myProguardConfigFilePathField.setText(FileUtil.toSystemDependentName(proguardCfgPath)); final String includeSystemProguardFile = properties.getValue(INCLUDE_SYSTEM_PROGUARD_FILE_PROPERTY); @@ -274,35 +267,11 @@ class ApkStep extends ExportSignedPackageWizardStep { @SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"}) private void createApk(File destFile) throws IOException, GeneralSecurityException { - final String srcApkPath = AndroidRootUtil.getApkPath(myWizard.getFacet()) + AndroidPackagingCompiler.UNSIGNED_SUFFIX; + final String srcApkPath = AndroidCompileUtil.getUnsignedApkPath(myWizard.getFacet()); final File srcApk = new File(FileUtil.toSystemDependentName(srcApkPath)); if (myWizard.isSigned()) { - FileOutputStream fos = new FileOutputStream(destFile); - PrivateKey privateKey = myWizard.getPrivateKey(); - assert privateKey != null; - X509Certificate certificate = myWizard.getCertificate(); - assert certificate != null; - SignedJarBuilder builder = new SafeSignedJarBuilder(fos, privateKey, certificate, destFile.getPath()); - FileInputStream fis = new FileInputStream(srcApk); - try { - builder.writeZip(fis, null); - builder.close(); - } - finally { - try { - fis.close(); - } - catch (IOException ignored) { - } - finally { - try { - fos.close(); - } - catch (IOException ignored) { - } - } - } + AndroidCommonUtils.signApk(srcApk, destFile, myWizard.getPrivateKey(), myWizard.getCertificate()); } else { FileUtil.copy(srcApk, destFile); diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/ExportSignedPackageWizardStep.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/ExportSignedPackageWizardStep.java index 8e542c7a8feb..a676bb78f1cb 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/ExportSignedPackageWizardStep.java +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/ExportSignedPackageWizardStep.java @@ -18,11 +18,9 @@ package org.jetbrains.android.exportSignedPackage; import com.intellij.ide.wizard.CommitStepException; import com.intellij.ide.wizard.StepAdapter; -import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.annotations.Nullable; import javax.swing.*; -import java.util.Arrays; /** * @author Eugene.Kudelevsky @@ -44,42 +42,8 @@ public abstract class ExportSignedPackageWizardStep extends StepAdapter { public abstract String getHelpId(); - protected static void checkNewPassword(JPasswordField passwordField, JPasswordField confirmedPasswordField) throws CommitStepException { - char[] password = passwordField.getPassword(); - char[] confirmedPassword = confirmedPasswordField.getPassword(); - try { - checkPassword(password); - if (password.length < 6) { - throw new CommitStepException(AndroidBundle.message("android.export.package.incorrect.password.length")); - } - if (!Arrays.equals(password, confirmedPassword)) { - throw new CommitStepException(AndroidBundle.message("android.export.package.passwords.not.match.error")); - } - } - finally { - Arrays.fill(password, '\0'); - Arrays.fill(confirmedPassword, '\0'); - } - } - protected abstract void commitForNext() throws CommitStepException; - protected static void checkPassword(char[] password) throws CommitStepException { - if (password.length == 0) { - throw new CommitStepException(AndroidBundle.message("android.export.package.specify.password.error")); - } - } - - protected static void checkPassword(JPasswordField passwordField) throws CommitStepException { - char[] password = passwordField.getPassword(); - try { - checkPassword(password); - } - finally { - Arrays.fill(password, '\0'); - } - } - @Override public Icon getIcon() { return null; diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/InitialKeyStep.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/InitialKeyStep.java index fc9e6de8540b..9a20ae2b17e0 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/InitialKeyStep.java +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/InitialKeyStep.java @@ -22,6 +22,7 @@ import com.intellij.openapi.ui.Messages; import com.intellij.ui.CollectionComboBoxModel; import com.intellij.util.ui.UIUtil; import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidUtils; import javax.swing.*; import java.awt.event.ActionEvent; @@ -132,7 +133,7 @@ class InitialKeyStep extends ExportSignedPackageWizardStep { if (myAliasCombo.getSelectedItem() == null) { throw new CommitStepException(AndroidBundle.message("android.extract.package.select.key.alias.error")); } - checkPassword(password); + AndroidUtils.checkPassword(password); String alias = (String)myAliasCombo.getSelectedItem(); loadKey(alias, password); } diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/KeystoreStep.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/KeystoreStep.java index 1cd022ce4fad..f5a8cc6b02bb 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/KeystoreStep.java +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/KeystoreStep.java @@ -26,6 +26,7 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidUtils; import org.jetbrains.android.util.SaveFileListener; import org.jetbrains.annotations.Nullable; @@ -102,7 +103,7 @@ class KeystoreStep extends ExportSignedPackageWizardStep { private KeyStore checkExistingKeystoreOptionsAndCreateKeystore(File keystoreFile) throws CommitStepException { char[] password = myKeystorePasswordField.getPassword(); FileInputStream fis = null; - checkPassword(password); + AndroidUtils.checkPassword(password); if (!keystoreFile.isFile()) { throw new CommitStepException(AndroidBundle.message("android.cannot.find.file.error", keystoreFile.getPath())); } @@ -129,7 +130,7 @@ class KeystoreStep extends ExportSignedPackageWizardStep { } private void checkNewKeystoreOptions(File keystoreFile) throws CommitStepException { - checkNewPassword(myKeystorePasswordField, myConfirmKeystorePasswordField); + AndroidUtils.checkNewPassword(myKeystorePasswordField, myConfirmKeystorePasswordField); if (keystoreFile.exists()) { throw new CommitStepException(AndroidBundle.message( diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.form b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.form similarity index 99% rename from plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.form rename to plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.form index 74c03cda9970..a9cef39e58cb 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.form +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.form @@ -1,5 +1,5 @@ -
+ @@ -71,7 +71,7 @@ - + diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.java new file mode 100644 index 000000000000..18558faf89f9 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.java @@ -0,0 +1,270 @@ +package org.jetbrains.android.exportSignedPackage; + +import com.android.jarutils.DebugKeyProvider; +import com.android.jarutils.KeystoreHelper; +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.ide.wizard.CommitStepException; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Eugene.Kudelevsky + */ +public abstract class NewKeyForm { + private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.exportSignedPackage.NewKeyForm"); + + private JPanel myContentPanel; + private JTextField myAliasField; + private JPasswordField myKeyPasswordField; + private JPasswordField myConfirmKeyPasswordField; + private JSpinner myValiditySpinner; + private JTextField myFirstAndLastNameField; + private JTextField myOrganizationUnitField; + private JTextField myCityField; + private JTextField myStateOrProvinceField; + private JTextField myCountryCodeField; + private JPanel myCertificatePanel; + private JTextField myOrganizationField; + + private KeyStore myKeyStore; + private PrivateKey myPrivateKey; + private X509Certificate myCertificate; + + public NewKeyForm() { + myValiditySpinner.setModel(new SpinnerNumberModel(25, 1, 1000, 1)); + } + + private int getValidity() { + SpinnerNumberModel model = (SpinnerNumberModel)myValiditySpinner.getModel(); + return model.getNumber().intValue(); + } + + public void init() { + myAliasField.setText(generateAlias()); + } + + public JPanel getContentPanel() { + return myContentPanel; + } + + private boolean findNonEmptyCertificateField() { + for (Component component : myCertificatePanel.getComponents()) { + if (component instanceof JTextField) { + if (((JTextField)component).getText().trim().length() > 0) { + return true; + } + } + } + return false; + } + + public void createKey() throws CommitStepException { + if (getKeyAlias().length() == 0) { + throw new CommitStepException(AndroidBundle.message("android.export.package.specify.key.alias.error")); + } + AndroidUtils.checkNewPassword(myKeyPasswordField, myConfirmKeyPasswordField); + if (!findNonEmptyCertificateField()) { + throw new CommitStepException(AndroidBundle.message("android.export.package.specify.certificate.field.error")); + } + doCreateKey(); + } + + @NotNull + private String generateAlias() { + List aliasList = getExistingKeyAliasList(); + String prefix = "key"; + if (aliasList == null) { + return prefix + '0'; + } + Set aliasSet = new HashSet(); + for (String alias : aliasList) { + aliasSet.add(alias.toLowerCase()); + } + for (int i = 0; ; i++) { + String alias = prefix + i; + if (!aliasSet.contains(alias)) { + return alias; + } + } + } + + @Nullable + protected abstract List getExistingKeyAliasList(); + + private static void buildDName(StringBuilder builder, String prefix, JTextField textField) { + if (textField != null) { + String value = textField.getText().trim(); + if (value.length() > 0) { + if (builder.length() > 0) { + builder.append(","); + } + builder.append(prefix); + builder.append('='); + builder.append(value); + } + } + } + + private String getDName() { + StringBuilder builder = new StringBuilder(); + buildDName(builder, "CN", myFirstAndLastNameField); + buildDName(builder, "OU", myOrganizationUnitField); + buildDName(builder, "O", myOrganizationField); + buildDName(builder, "L", myCityField); + buildDName(builder, "ST", myStateOrProvinceField); + buildDName(builder, "C", myCountryCodeField); + return builder.toString(); + } + + + + private void doCreateKey() throws CommitStepException { + String keystoreLocation = getKeyStoreLocation(); + char[] keystorePassword = getKeyStorePassword(); + char[] keyPassword = getKeyPassword(); + String keyAlias = getKeyAlias(); + String dname = getDName(); + assert dname != null; + boolean createdStore = false; + final StringBuilder errorBuilder = new StringBuilder(); + final StringBuilder outBuilder = new StringBuilder(); + try { + createdStore = KeystoreHelper + .createNewStore(keystoreLocation, null, new String(keystorePassword), keyAlias, new String(keyPassword), dname, getValidity(), + new DebugKeyProvider.IKeyGenOutput() { + public void err(String message) { + errorBuilder.append(message).append('\n'); + LOG.info("Error: " + message); + } + + public void out(String message) { + outBuilder.append(message).append('\n'); + LOG.info(message); + } + }); + } + catch (Exception e) { + LOG.info(e); + errorBuilder.append(e.getMessage()).append('\n'); + } + normalizeBuilder(errorBuilder); + normalizeBuilder(outBuilder); + try { + if (createdStore) { + if (errorBuilder.length() > 0) { + String prefix = AndroidBundle.message("android.create.new.key.error.prefix"); + Messages.showErrorDialog(myContentPanel, prefix + '\n' + errorBuilder.toString()); + } + } + else { + if (errorBuilder.length() > 0) { + throw new CommitStepException(errorBuilder.toString()); + } + if (outBuilder.length() > 0) { + throw new CommitStepException(outBuilder.toString()); + } + throw new CommitStepException(AndroidBundle.message("android.cannot.create.new.key.error")); + } + PropertiesComponent.getInstance(getProject()).setValue(KeystoreStep.DEFAULT_KEYSTORE_LOCATION, keystoreLocation); + loadKeystoreAndKey(keystoreLocation, keystorePassword, keyAlias, keyPassword); + } + finally { + Arrays.fill(keystorePassword, '\0'); + Arrays.fill(keyPassword, '\0'); + } + } + + @NotNull + public char[] getKeyPassword() { + return myKeyPasswordField.getPassword(); + } + + @NotNull + public String getKeyAlias() { + return myAliasField.getText().trim(); + } + + @NotNull + protected abstract Project getProject(); + + @NotNull + protected abstract char[] getKeyStorePassword(); + + @NotNull + protected abstract String getKeyStoreLocation(); + + private void loadKeystoreAndKey(String keystoreLocation, char[] keystorePassword, String keyAlias, char[] keyPassword) + throws CommitStepException { + FileInputStream fis = null; + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + fis = new FileInputStream(new File(keystoreLocation)); + keyStore.load(fis, keystorePassword); + myKeyStore = keyStore; + KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(keyAlias, new KeyStore.PasswordProtection(keyPassword)); + if (entry == null) { + throw new CommitStepException(AndroidBundle.message("android.extract.package.cannot.find.key.error", keyAlias)); + } + PrivateKey privateKey = entry.getPrivateKey(); + Certificate certificate = entry.getCertificate(); + if (privateKey == null || certificate == null) { + throw new CommitStepException(AndroidBundle.message("android.extract.package.cannot.find.key.error", keyAlias)); + } + PropertiesComponent.getInstance(getProject()).setValue(InitialKeyStep.DEFAULT_KEY_ALIAS, keyAlias); + myPrivateKey = privateKey; + myCertificate = (X509Certificate)certificate; + } + catch (Exception e) { + throw new CommitStepException("Error: " + e.getMessage()); + } + finally { + if (fis != null) { + try { + fis.close(); + } + catch (IOException ignored) { + } + } + } + } + + private static void normalizeBuilder(StringBuilder builder) { + if (builder.length() > 0) { + builder.deleteCharAt(builder.length() - 1); + } + } + + @Nullable + public KeyStore getKeyStore() { + return myKeyStore; + } + + @Nullable + public PrivateKey getPrivateKey() { + return myPrivateKey; + } + + @Nullable + public X509Certificate getCertificate() { + return myCertificate; + } +} diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.java index bb4887969ae8..dcfb4d96f0fe 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.java +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.java @@ -16,79 +16,37 @@ package org.jetbrains.android.exportSignedPackage; -import com.android.jarutils.DebugKeyProvider; -import com.android.jarutils.KeystoreHelper; -import com.intellij.ide.util.PropertiesComponent; import com.intellij.ide.wizard.CommitStepException; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.ui.Messages; -import org.jetbrains.android.util.AndroidBundle; +import com.intellij.openapi.project.Project; import org.jetbrains.annotations.NotNull; import javax.swing.*; -import java.awt.*; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; import java.security.KeyStore; import java.security.PrivateKey; -import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** * @author Eugene.Kudelevsky */ public class NewKeyStep extends ExportSignedPackageWizardStep { - private static final Logger LOG = Logger.getInstance("#com.jetbrains.android.exportSignedPackage.NewKeyStep"); - - private JPanel myContentPanel; - private JTextField myAliasField; - private JPasswordField myKeyPasswordField; - private JPasswordField myConfirmKeyPasswordField; - private JSpinner myValiditySpinner; - private JTextField myFirstAndLastNameField; - private JTextField myOrganizationUnitField; - private JTextField myCityField; - private JTextField myStateOrProvinceField; - private JTextField myCountryCodeField; - private JPanel myCertificatePanel; - private JTextField myOrganizationField; private final ExportSignedPackageWizard myWizard; + private final NewKeyForm myNewKeyForm; public NewKeyStep(ExportSignedPackageWizard wizard) { myWizard = wizard; - myValiditySpinner.setModel(new SpinnerNumberModel(25, 1, 1000, 1)); - } - - private int getValidity() { - SpinnerNumberModel model = (SpinnerNumberModel)myValiditySpinner.getModel(); - return model.getNumber().intValue(); + myNewKeyForm = new MyNewKeyForm(); } @Override public void _init() { - myAliasField.setText(generateAlias()); + myNewKeyForm.init(); } @Override public JComponent getComponent() { - return myContentPanel; - } - - private boolean findNonEmptyCertificateField() { - for (Component component : myCertificatePanel.getComponents()) { - if (component instanceof JTextField) { - if (((JTextField)component).getText().trim().length() > 0) { - return true; - } - } - } - return false; + return myNewKeyForm.getContentPanel(); } @Override @@ -98,157 +56,42 @@ public class NewKeyStep extends ExportSignedPackageWizardStep { @Override protected void commitForNext() throws CommitStepException { - if (myAliasField.getText().trim().length() == 0) { - throw new CommitStepException(AndroidBundle.message("android.export.package.specify.key.alias.error")); - } - checkNewPassword(myKeyPasswordField, myConfirmKeyPasswordField); - if (!findNonEmptyCertificateField()) { - throw new CommitStepException(AndroidBundle.message("android.export.package.specify.certificate.field.error")); - } - createKey(); + myNewKeyForm.createKey(); + final KeyStore keyStore = myNewKeyForm.getKeyStore(); + final PrivateKey privateKey = myNewKeyForm.getPrivateKey(); + final X509Certificate certificate = myNewKeyForm.getCertificate(); + assert keyStore != null && privateKey != null && certificate != null; + myWizard.setKeystore(keyStore); + myWizard.setPrivateKey(privateKey); + myWizard.setCertificate(certificate); } - @NotNull - private String generateAlias() { - List aliasList = myWizard.getKeyAliasList(); - String prefix = "key"; - if (aliasList == null) { - return prefix + '0'; + private class MyNewKeyForm extends NewKeyForm { + @Override + protected List getExistingKeyAliasList() { + return myWizard.getKeyAliasList(); } - Set aliasSet = new HashSet(); - for (String alias : aliasList) { - aliasSet.add(alias.toLowerCase()); - } - for (int i = 0; ; i++) { - String alias = prefix + i; - if (!aliasSet.contains(alias)) { - return alias; - } - } - } - private static void buildDName(StringBuilder builder, String prefix, JTextField textField) { - if (textField != null) { - String value = textField.getText().trim(); - if (value.length() > 0) { - if (builder.length() > 0) { - builder.append(","); - } - builder.append(prefix); - builder.append('='); - builder.append(value); - } + @NotNull + @Override + protected String getKeyStoreLocation() { + final String location = myWizard.getKeystoreLocation(); + assert location != null; + return location; } - } - private String getDName() { - StringBuilder builder = new StringBuilder(); - buildDName(builder, "CN", myFirstAndLastNameField); - buildDName(builder, "OU", myOrganizationUnitField); - buildDName(builder, "O", myOrganizationField); - buildDName(builder, "L", myCityField); - buildDName(builder, "ST", myStateOrProvinceField); - buildDName(builder, "C", myCountryCodeField); - return builder.toString(); - } + @NotNull + @Override + protected char[] getKeyStorePassword() { + final char[] password = myWizard.getKeystorePassword(); + assert password != null; + return password; + } - private void createKey() throws CommitStepException { - String keystoreLocation = myWizard.getKeystoreLocation(); - assert keystoreLocation != null; - char[] keystorePassword = myWizard.getKeystorePassword(); - assert keystorePassword != null; - char[] keyPassword = myKeyPasswordField.getPassword(); - String keyAlias = myAliasField.getText().trim(); - String dname = getDName(); - assert dname != null; - boolean createdStore = false; - final StringBuilder errorBuilder = new StringBuilder(); - final StringBuilder outBuilder = new StringBuilder(); - try { - createdStore = KeystoreHelper - .createNewStore(keystoreLocation, null, new String(keystorePassword), keyAlias, new String(keyPassword), dname, getValidity(), - new DebugKeyProvider.IKeyGenOutput() { - public void err(String message) { - errorBuilder.append(message).append('\n'); - LOG.info("Error: " + message); - } - - public void out(String message) { - outBuilder.append(message).append('\n'); - LOG.info(message); - } - }); - } - catch (Exception e) { - LOG.info(e); - errorBuilder.append(e.getMessage()).append('\n'); - } - normalizeBuilder(errorBuilder); - normalizeBuilder(outBuilder); - try { - if (createdStore) { - if (errorBuilder.length() > 0) { - String prefix = AndroidBundle.message("android.create.new.key.error.prefix"); - Messages.showErrorDialog(myContentPanel, prefix + '\n' + errorBuilder.toString()); - } - } - else { - if (errorBuilder.length() > 0) { - throw new CommitStepException(errorBuilder.toString()); - } - if (outBuilder.length() > 0) { - throw new CommitStepException(outBuilder.toString()); - } - throw new CommitStepException(AndroidBundle.message("android.cannot.create.new.key.error")); - } - PropertiesComponent.getInstance(myWizard.getProject()).setValue(KeystoreStep.DEFAULT_KEYSTORE_LOCATION, keystoreLocation); - loadKeystoreAndKey(keystoreLocation, keystorePassword, keyAlias, keyPassword); - } - finally { - Arrays.fill(keystorePassword, '\0'); - Arrays.fill(keyPassword, '\0'); - } - } - - private void loadKeystoreAndKey(String keystoreLocation, char[] keystorePassword, String keyAlias, char[] keyPassword) - throws CommitStepException { - FileInputStream fis = null; - try { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - fis = new FileInputStream(new File(keystoreLocation)); - keyStore.load(fis, keystorePassword); - myWizard.setKeystore(keyStore); - KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(keyAlias, new KeyStore.PasswordProtection(keyPassword)); - if (entry == null) { - throw new CommitStepException(AndroidBundle.message("android.extract.package.cannot.find.key.error", keyAlias)); - } - PrivateKey privateKey = entry.getPrivateKey(); - Certificate certificate = entry.getCertificate(); - if (privateKey == null || certificate == null) { - throw new CommitStepException(AndroidBundle.message("android.extract.package.cannot.find.key.error", keyAlias)); - } - PropertiesComponent.getInstance(myWizard.getProject()).setValue(InitialKeyStep.DEFAULT_KEY_ALIAS, keyAlias); - myWizard.setPrivateKey(privateKey); - myWizard.setCertificate((X509Certificate)certificate); - - } - catch (Exception e) { - throw new CommitStepException("Error: " + e.getMessage()); - } - finally { - if (fis != null) { - try { - fis.close(); - } - catch (IOException ignored) { - } - } - } - } - - private static void normalizeBuilder(StringBuilder builder) { - if (builder.length() > 0) { - builder.deleteCharAt(builder.length() - 1); + @NotNull + @Override + protected Project getProject() { + return myWizard.getProject(); } } } diff --git a/plugins/android/src/org/jetbrains/android/util/AndroidUtils.java b/plugins/android/src/org/jetbrains/android/util/AndroidUtils.java index 2dc112b8b14d..9cf55702bbe1 100644 --- a/plugins/android/src/org/jetbrains/android/util/AndroidUtils.java +++ b/plugins/android/src/org/jetbrains/android/util/AndroidUtils.java @@ -37,6 +37,7 @@ import com.intellij.facet.FacetManager; import com.intellij.facet.ModifiableFacetModel; import com.intellij.facet.ProjectFacetManager; import com.intellij.ide.util.DefaultPsiElementCellRenderer; +import com.intellij.ide.wizard.CommitStepException; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; @@ -85,10 +86,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; /** * @author yole, coyote @@ -714,4 +712,47 @@ public class AndroidUtils { } return result; } + + public static void checkNewPassword(JPasswordField passwordField, JPasswordField confirmedPasswordField) throws CommitStepException { + char[] password = passwordField.getPassword(); + char[] confirmedPassword = confirmedPasswordField.getPassword(); + try { + checkPassword(password); + if (password.length < 6) { + throw new CommitStepException(AndroidBundle.message("android.export.package.incorrect.password.length")); + } + if (!Arrays.equals(password, confirmedPassword)) { + throw new CommitStepException(AndroidBundle.message("android.export.package.passwords.not.match.error")); + } + } + finally { + Arrays.fill(password, '\0'); + Arrays.fill(confirmedPassword, '\0'); + } + } + + public static void checkPassword(char[] password) throws CommitStepException { + if (password.length == 0) { + throw new CommitStepException(AndroidBundle.message("android.export.package.specify.password.error")); + } + } + + public static void checkPassword(JPasswordField passwordField) throws CommitStepException { + char[] password = passwordField.getPassword(); + try { + checkPassword(password); + } + finally { + Arrays.fill(password, '\0'); + } + } + + @NotNull + public static List toList(@NotNull Enumeration enumeration) { + final List result = new ArrayList(); + while (enumeration.hasMoreElements()) { + result.add(enumeration.nextElement()); + } + return result; + } }