blob: c5843b3d53c8179df2e33911e7efd5247449f683 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.ide.browsers;
import com.intellij.CommonBundle;
import com.intellij.Patches;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.util.ExecUtil;
import com.intellij.ide.BrowserUtil;
import com.intellij.ide.GeneralSettings;
import com.intellij.ide.IdeBundle;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.StandardFileSystems;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.ui.GuiUtils;
import com.intellij.util.ArrayUtil;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.URLUtil;
import com.intellij.util.io.ZipUtil;
import com.intellij.util.ui.OptionsDialog;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class BrowserLauncherAppless extends BrowserLauncher {
static final Logger LOG = Logger.getInstance(BrowserLauncherAppless.class);
private static boolean isDesktopActionSupported(Desktop.Action action) {
return !Patches.SUN_BUG_ID_6457572 && !Patches.SUN_BUG_ID_6486393 &&
Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(action);
}
public static boolean canUseSystemDefaultBrowserPolicy() {
return isDesktopActionSupported(Desktop.Action.BROWSE) ||
SystemInfo.isMac || SystemInfo.isWindows ||
(SystemInfo.isUnix && SystemInfo.hasXdgOpen());
}
private static GeneralSettings getGeneralSettingsInstance() {
if (ApplicationManager.getApplication() != null) {
GeneralSettings settings = GeneralSettings.getInstance();
if (settings != null) {
return settings;
}
}
return new GeneralSettings();
}
@Nullable
private static List<String> getDefaultBrowserCommand() {
if (SystemInfo.isWindows) {
return Arrays.asList(ExecUtil.getWindowsShellName(), "/c", "start", GeneralCommandLine.inescapableQuote(""));
}
else if (SystemInfo.isMac) {
return Collections.singletonList(ExecUtil.getOpenCommandPath());
}
else if (SystemInfo.isUnix && SystemInfo.hasXdgOpen()) {
return Collections.singletonList("xdg-open");
}
else {
return null;
}
}
@Override
public void open(@NotNull String url) {
openOrBrowse(url, false, null);
}
@Override
public void browse(@NotNull File file) {
browse(VfsUtil.toUri(file));
}
@Override
public void browse(@NotNull URI uri) {
browse(uri, null);
}
public void browse(@NotNull URI uri, @Nullable Project project) {
LOG.debug("Launch browser: [" + uri + "]");
GeneralSettings settings = getGeneralSettingsInstance();
if (settings.isUseDefaultBrowser()) {
boolean tryToUseCli = true;
if (isDesktopActionSupported(Desktop.Action.BROWSE)) {
try {
Desktop.getDesktop().browse(uri);
LOG.debug("Browser launched using JDK 1.6 API");
return;
}
catch (Exception e) {
LOG.warn("Error while using Desktop API, fallback to CLI", e);
// if "No application knows how to open", then we must not try to use OS open
tryToUseCli = !e.getMessage().contains("Error code: -10814");
}
}
if (tryToUseCli) {
List<String> command = getDefaultBrowserCommand();
if (command != null) {
doLaunch(uri.toString(), command, null, project, ArrayUtil.EMPTY_STRING_ARRAY, null);
return;
}
}
}
browseUsingNotSystemDefaultBrowserPolicy(uri, settings, project);
}
protected void browseUsingNotSystemDefaultBrowserPolicy(@NotNull URI uri, @NotNull GeneralSettings settings, @Nullable Project project) {
browseUsingPath(uri.toString(), settings.getBrowserPath(), null, project, ArrayUtil.EMPTY_STRING_ARRAY);
}
private void openOrBrowse(@NotNull String url, boolean browse, @Nullable Project project) {
url = url.trim();
if (url.startsWith("jar:")) {
String files = extractFiles(url);
if (files == null) {
return;
}
url = files;
}
URI uri;
if (BrowserUtil.isAbsoluteURL(url)) {
uri = VfsUtil.toUri(url);
}
else {
File file = new File(url);
if (!browse && isDesktopActionSupported(Desktop.Action.OPEN)) {
if (!file.exists()) {
showError(IdeBundle.message("error.file.does.not.exist", file.getPath()), null, null, null, null);
return;
}
try {
Desktop.getDesktop().open(file);
return;
}
catch (IOException e) {
LOG.debug(e);
}
}
browse(file);
return;
}
if (uri == null) {
showError(IdeBundle.message("error.malformed.url", url), null, project, null, null);
}
else {
browse(uri, project);
}
}
@Nullable
private static String extractFiles(String url) {
try {
int sharpPos = url.indexOf('#');
String anchor = "";
if (sharpPos != -1) {
anchor = url.substring(sharpPos);
url = url.substring(0, sharpPos);
}
Pair<String, String> pair = URLUtil.splitJarUrl(url);
if (pair == null) return null;
File jarFile = new File(FileUtil.toSystemDependentName(pair.first));
if (!jarFile.canRead()) return null;
String jarUrl = StandardFileSystems.FILE_PROTOCOL_PREFIX + FileUtil.toSystemIndependentName(jarFile.getPath());
String jarLocationHash = jarFile.getName() + "." + Integer.toHexString(jarUrl.hashCode());
final File outputDir = new File(getExtractedFilesDir(), jarLocationHash);
final String currentTimestamp = String.valueOf(new File(jarFile.getPath()).lastModified());
final File timestampFile = new File(outputDir, ".idea.timestamp");
String previousTimestamp = null;
if (timestampFile.exists()) {
previousTimestamp = FileUtilRt.loadFile(timestampFile);
}
if (!currentTimestamp.equals(previousTimestamp)) {
final Ref<Boolean> extract = new Ref<Boolean>();
Runnable r = new Runnable() {
@Override
public void run() {
final ConfirmExtractDialog dialog = new ConfirmExtractDialog();
if (dialog.isToBeShown()) {
dialog.show();
extract.set(dialog.isOK());
}
else {
dialog.close(DialogWrapper.OK_EXIT_CODE);
extract.set(true);
}
}
};
try {
GuiUtils.runOrInvokeAndWait(r);
}
catch (InvocationTargetException ignored) {
extract.set(false);
}
catch (InterruptedException ignored) {
extract.set(false);
}
if (!extract.get()) {
return null;
}
boolean closeZip = true;
final ZipFile zipFile = new ZipFile(jarFile);
try {
ZipEntry entry = zipFile.getEntry(pair.second);
if (entry == null) {
return null;
}
InputStream is = zipFile.getInputStream(entry);
ZipUtil.extractEntry(entry, is, outputDir);
closeZip = false;
}
finally {
if (closeZip) {
zipFile.close();
}
}
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
new Task.Backgroundable(null, "Extracting files...", true) {
@Override
public void run(@NotNull final ProgressIndicator indicator) {
final int size = zipFile.size();
final int[] counter = new int[]{0};
class MyFilter implements FilenameFilter {
private final Set<File> myImportantDirs = ContainerUtil.newHashSet(outputDir, new File(outputDir, "resources"));
private final boolean myImportantOnly;
private MyFilter(boolean importantOnly) {
myImportantOnly = importantOnly;
}
@Override
public boolean accept(@NotNull File dir, @NotNull String name) {
indicator.checkCanceled();
boolean result = myImportantOnly == myImportantDirs.contains(dir);
if (result) {
indicator.setFraction(((double)counter[0]) / size);
counter[0]++;
}
return result;
}
}
try {
try {
ZipUtil.extract(zipFile, outputDir, new MyFilter(true));
ZipUtil.extract(zipFile, outputDir, new MyFilter(false));
FileUtil.writeToFile(timestampFile, currentTimestamp);
}
finally {
zipFile.close();
}
}
catch (IOException ignore) { }
}
}.queue();
}
});
}
return VfsUtilCore.pathToUrl(FileUtil.toSystemIndependentName(new File(outputDir, pair.second).getPath())) + anchor;
}
catch (IOException e) {
LOG.warn(e);
Messages.showErrorDialog("Cannot extract files: " + e.getMessage(), "Error");
return null;
}
}
private static File getExtractedFilesDir() {
return new File(PathManager.getSystemPath(), "ExtractedFiles");
}
public static void clearExtractedFiles() {
FileUtil.delete(getExtractedFilesDir());
}
private static class ConfirmExtractDialog extends OptionsDialog {
private ConfirmExtractDialog() {
super(null);
setTitle("Confirmation");
init();
}
@Override
protected boolean isToBeShown() {
return getGeneralSettingsInstance().isConfirmExtractFiles();
}
@Override
protected void setToBeShown(boolean value, boolean onOk) {
getGeneralSettingsInstance().setConfirmExtractFiles(value);
}
@Override
protected boolean shouldSaveOptionsOnCancel() {
return true;
}
@Override
@NotNull
protected Action[] createActions() {
setOKButtonText(CommonBundle.getYesButtonText());
return new Action[]{getOKAction(), getCancelAction()};
}
@Override
protected JComponent createCenterPanel() {
JPanel panel = new JPanel(new BorderLayout());
String message = "The files are inside an archive, do you want them to be extracted?";
JLabel label = new JLabel(message);
label.setIconTextGap(10);
label.setIcon(Messages.getQuestionIcon());
panel.add(label, BorderLayout.CENTER);
panel.add(Box.createVerticalStrut(10), BorderLayout.SOUTH);
return panel;
}
}
@Override
public void browse(@NotNull String url, @Nullable WebBrowser browser) {
browse(url, browser, null);
}
@Override
public void browse(@NotNull String url, @Nullable WebBrowser browser, @Nullable Project project) {
if (browser == null) {
openOrBrowse(url, true, project);
}
else {
for (UrlOpener urlOpener : UrlOpener.EP_NAME.getExtensions()) {
if (urlOpener.openUrl(browser, url, project)) {
return;
}
}
}
}
@Override
public boolean browseUsingPath(@Nullable final String url,
@Nullable String browserPath,
@Nullable final WebBrowser browser,
@Nullable final Project project,
@NotNull final String[] additionalParameters) {
Runnable launchTask = null;
if (browserPath == null && browser != null) {
browserPath = PathUtil.toSystemDependentName(browser.getPath());
launchTask = new Runnable() {
@Override
public void run() {
browseUsingPath(url, null, browser, project, additionalParameters);
}
};
}
return doLaunch(url, browserPath, browser, project, additionalParameters, launchTask);
}
private boolean doLaunch(@Nullable String url,
@Nullable String browserPath,
@Nullable WebBrowser browser,
@Nullable Project project,
@NotNull String[] additionalParameters,
@Nullable Runnable launchTask) {
if (!checkPath(browserPath, browser, project, launchTask)) {
return false;
}
return doLaunch(url, BrowserUtil.getOpenBrowserCommand(browserPath, false), browser, project, additionalParameters, launchTask);
}
@Contract("null, _, _, _ -> false")
public boolean checkPath(@Nullable String browserPath, @Nullable WebBrowser browser, @Nullable Project project, @Nullable Runnable launchTask) {
if (!StringUtil.isEmptyOrSpaces(browserPath)) {
return true;
}
String message = browser != null ? browser.getBrowserNotFoundMessage() :
IdeBundle.message("error.please.specify.path.to.web.browser", CommonBundle.settingsActionPath());
showError(message, browser, project, IdeBundle.message("title.browser.not.found"), launchTask);
return false;
}
private boolean doLaunch(@Nullable String url,
@NotNull List<String> command,
@Nullable WebBrowser browser,
@Nullable Project project,
@NotNull String[] additionalParameters,
@Nullable Runnable launchTask) {
GeneralCommandLine commandLine = new GeneralCommandLine(command);
if (url != null && url.startsWith("jar:")) {
String files = extractFiles(url);
if (files == null) {
return false;
}
url = files;
}
if (url != null) {
commandLine.addParameter(url);
}
final BrowserSpecificSettings browserSpecificSettings = browser == null ? null : browser.getSpecificSettings();
if (browserSpecificSettings != null) {
commandLine.getEnvironment().putAll(browserSpecificSettings.getEnvironmentVariables());
}
addArgs(commandLine, browserSpecificSettings, additionalParameters);
try {
Process process = commandLine.createProcess();
checkCreatedProcess(browser, project, commandLine, process, launchTask);
return true;
}
catch (ExecutionException e) {
showError(e.getMessage(), browser, project, null, null);
return false;
}
}
protected void checkCreatedProcess(@Nullable WebBrowser browser,
@Nullable Project project,
@NotNull GeneralCommandLine commandLine,
@NotNull Process process,
@Nullable Runnable launchTask) {
}
protected void showError(@Nullable String error, @Nullable WebBrowser browser, @Nullable Project project, String title, @Nullable Runnable launchTask) {
// Not started yet. Not able to show message up. (Could happen in License panel under Linux).
LOG.warn(error);
}
private static void addArgs(@NotNull GeneralCommandLine command, @Nullable BrowserSpecificSettings settings, @NotNull String[] additional) {
List<String> specific = settings == null ? Collections.<String>emptyList() : settings.getAdditionalParameters();
if (specific.size() + additional.length > 0) {
if (isOpenCommandUsed(command)) {
if (BrowserUtil.isOpenCommandSupportArgs()) {
command.addParameter("--args");
}
else {
LOG.warn("'open' command doesn't allow to pass command line arguments so they will be ignored: " +
StringUtil.join(specific, ", ") + " " + Arrays.toString(additional));
return;
}
}
command.addParameters(specific);
command.addParameters(additional);
}
}
public static boolean isOpenCommandUsed(@NotNull GeneralCommandLine command) {
return SystemInfo.isMac && ExecUtil.getOpenCommandPath().equals(command.getExePath());
}
}