JBrowser: реинкарнация MozSwing

imageВспомним, что же такое MozSwing. MozSwing — единственное адекватное (по моему мнению) бесплатное и кросс-платформенное решение для встраивания браузера как компонента swing. Но, как ни печально это признавать, проект умер еще на той стадии, когда в нем оставалось слишком много ошибок. Эти ошибки, а так же просчеты в архитектуре, не позволяют использовать это решение «как есть», для ваших проектов. С непреодолимым желанием исправить это, я взялся за работу и кое-что у меня получилось.

(Статья о интеграции браузера Mozilla, как Swing компонента).

Почему плох MozSwing

Нет-нет, не подумайте будто я считаю MozSwing полным отстоем, это громадная работа за которую нужно сказать огромное спасибо (моя работа в разы меньше), но все же меня не устраивает MozSwing и вот почему:

  1. Поддержка xulrunner`а только версии 1.9 (который, к слову, не поддерживает русские буквы в пути для windows-систем), когда есть версия 1.9.2.
  2. Огромное количество разрывающих мозг статических классов и методов, из-за чего MozSwing предстает перед нами нерушимой скалой. Для того, чтобы что-либо в нем изменить, приходится менять байт-код уже загруженных классов, потому что наследование в случае MozSwing не работает.
  3. Невозможность явно переопределить nsIWindowCreator (класс, реализующий этот интерфейс, занимается созданием окон для браузера), из-за чего приходится выдумывать костыли для создания браузеров как вкладок, поскольку MozSwing предпочитает открывать новые странички в новых окнах. Вот, например, так я заменяю nsIWindowCreator после инициализации:

    nsIWindowWatcher winWatcher = XPCOMUtils.getService("@mozilla.org/embedcomp/window-watcher;1", nsIWindowWatcher.class); //$NON-NLS-1$
    winWatcher.setWindowCreator(wndCreator);

    * This source code was highlighted with Source Code Highlighter.

    Однако, потенциально здесь скрыта ошибка, поскольку MozSwing любит статические классы, методы, и внутри ссылается на свой nsIWindowCreator.

  4. Некоторые, скажем так, очень сомнительные решения в коде (исходники MozSwing: на всякий случай создается пул в 3 окна, чтобы потом из этого пула взять окно для встройки в него браузера):

    /**
    * When mozilla does a callback to createChromeWindow()
    * we need to create a swing window. But doing this on
    * Swing thread using invokeAndWait sometimes ends
    * with deadlock in AWT.
    * Therefore we keep a list of precreated windows
    * in case we will need them.
    */
    private List<IMozillaWindow> precreatedWins = new LinkedList<IMozillaWindow>();

    public void ensurePrecreatedWindows() {
    ensurePrecreatedWindows(3);
    }
    public void ensurePrecreatedWindows(int winNum) {
    assert !isMozillaThread(); //has to be called from swing

    while (precreatedWins.size()<winNum) {

    if (winFactory==null) return;
    IMozillaWindow w = winFactory.create(false);
    if (!(w instanceof Component)) return;

    // w is instance of something we can work with
    precreatedWins.add(w);
    Component c = (Component)w;
    c.addNotify();
    }
    }

    * This source code was highlighted with Source Code Highlighter.

    Особенно пугает число три. Это как бы намекает нам на то, что трех окон в принципе должно хватить… Наверное…

  5. Для включения поддержки контекстных команд (конеткстного меню), приходится менять некоторые методы MozSwing`a. Например:

    @SuppressWarnings("deprecation")
    public static void replaceChromeAdapterMethod() {
    try {
    ClassPool classPool = ClassPool.getDefault();
    CtClass ctClass = classPool.get("org.mozilla.browser.impl.ChromeAdapter");
    CtMethod ctMethod = ctClass.getMethod("queryInterface", "(Ljava/lang/String;)Lorg/mozilla/interfaces/nsISupports;");
    ctMethod.setBody("{ return ru.redstonegroup.geo.gui.components.browser.impl.QueryInterfaceImpl.getInstance().queryInterface(this, $1); }");
    ctClass.toClass(QueryInterfaceImpl.class.getClassLoader());
    } catch (Throwable e) {
    logger.error(e.getMessage(), e);
    }
    }

    * This source code was highlighted with Source Code Highlighter.

  6. Постоянно роняет Sun JVM на некоторых linux системах (ubuntu, openSUSE).
  7. Запутанность исходников (к слову, скорее это связанно с запутанностью самой технологии XPCOM).
  8. Нет интеграции с maven.
  9. Тяжело интегрировать с IoC контейнером.
  10. Не возмжно создать окно без всяких рюшечек, их можно только спрятать.

JBrowser

Конечно, я не питаю иллюзий — мое решение не идеально: поскольку оно базируется на MozSwing`е, то и переняло множество его болячек. Если хотите, JBrowser это мое переосмысление MozSwing`а в значительной степени более подходящие для реальных систем. Ну, по крайней мере, мне мое API нравится в разы больше (хотя вам оно может совсем не понравиться).

Главной проблемой при знакомстве с MozSwing у меня было то, что в нем нет единой точки входа — создание браузера выполнялось как простое создание компонента (что-то вроде new MozillaWindow, простите, уже точно не вспомню). Да, это в каком-то смысле удобно, пока вам не нужно нечто большее, чем создание просто окна браузера, но как сконфигурировать создаваемый браузер? Один из вариантов — наследоваться от MozSwing-компонентов, лезть внутрь и копать-копать-копать…

Сходу мне было не понятно: как изменить параметры прокси-сервера для браузера? Через некоторое время выяснилось, что есть соответствующий класс MozillaConfig со статическим методом setProxy (или как-то так). О, боже мой, я бы это никогда не узнал, если бы не открыл исходники. В общем, для меня это все не очевидно.

Поэтому JBrowser это некая противоположность (на данный момент не полная) MozSwing в смысле дизайна. В JBrowser есть точка входа — интерфейс BrowserManager. Это самый верхний уровень интеграции xulrunner`а и swing`а. Класс реализующий интерфейс выполняет всю инициализацию, так сказать подготавливает почву для дальнейшей работы. Помимо инициализации, реализующий класс обязан предоставить вам по первому требованию некую реализацию интерфейса BrowserConfig, который позволяет регулировать политику работы всех браузеров (включать/отключать картинки, прокси и др.) Как раз то, что я хотел.

В противоположность верхнему уровню интеграции в JBrowser присутствует самый нижний уровень интеграции — так называемый «компонент-браузер». Любой компонент-браузер реализует интерфейс JBrowserComponent. Это интерфейс представляет собой композит, совмещающий в себе функционал Swing компонента и реализует интерфейс браузера.

/**
* Браузер встроенный в компонент Swing // Browser embedded in swing component
* @author caiiiycuk
*/
public interface JBrowserComponent<T extends Component> extends DisplayableComponent, Browser, NativeBrowser {
/**
* @return See {@link java.awt.Component}
*/
T getComponent();
...
}

* This source code was highlighted with Source Code Highlighter.

Есть JBrowserCanvas — базовая реализация этого интерфейса. Это не что иное, как Swing-компонент Canvas c встроенным в него браузером. Другие реализации компонента-браузера почти всегда оборачивают JBrowserCanvas (делегируют вызовы к нему). Например, так делает другой компонент-браузер JBrowserFrame (браузер и JFrame).

Между этими двумя противоположностями существует еще одно звено, которое объединяет все в единое целое — это factory-слой (фактори-слой). После создания верхнего слоя интеграции на основе BrowserManager могут быть созданы многочисленные фактори компонентов-браузеров, реализующие интерфейс ComponentFactory. Нормально, когда приложение содержит несколько таких фактори. Правильно настроенная фактори посредством своих методов создает конкретные реализации компонентов-браузеров. Допустим, я использую в своем приложении следующие фактори: JFrameBrowserFactory (создает браузер как новое окно), JTabbedBrowserFactory (создает браузер как новую вкладку). Благодаря этой схеме становится возможным легко решить проблему кастомизации создаваемых вами компонентов-браузеров.

Таким образом, вот вся цепочка работы с JBrowser: создать BrowserManager (кстати, для этого можно/нужно использовать билдер), создать хотя бы одну фактори для ваших комопонентов-браузеров и наконец, создать браузер с помощью фактори. Вот так выглядит самый простой вариант работы с JBrowser:

public class GettingStartedSnippet {

public static void main(String[] args) {
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();

JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize((int) (screenSize.getWidth() * 0.75f),
(int) (screenSize.getHeight() * 0.75f));
frame.setLocationRelativeTo(null);

BrowserManager browserManager =
new JBrowserBuilder().buildBrowserManager();

JComponentFactory<Canvas> canvasFactory = browserManager.getComponentFactory(JBrowserCanvas.class);
JBrowserComponent<?> browser = canvasFactory.createBrowser();

frame.getContentPane().add(browser.getComponent());
frame.setVisible(true);

browser.setUrl("http://code.google.com/p/jbrowser/");
}
}

* This source code was highlighted with Source Code Highlighter.

Почему фактори? Я считаю (может, я и не прав), что это очень удобно. Я, например, регистрирую свои фактори в IoC-контейнере и легко могу получить к ним доступ практически из любой части приложения, поэтому я могу хоть в самой последней менюшке сделать кнопку, создающую мне новую вкладку-браузер.
Плюс этой архитектуры в том, что вы можете переписать любой из этих трех уровней, и получить по-прежнему эффективно работающею систему.

Фичи, которых не было в MozSwing

Помимо полной переработки интерфейсов, добавились еще вот какие фичи:

  • Стало возможным легко получить favIcon открытой страницы (browser.getFavIcon()).
  • Стало возможным легко встроить контекстное меню.
  • Полная интеграция с maven. В связи с этим JBrowser легко подключить и легко собрать под вашу целевую систему (поменяв профиль в pom.xml). На данный момент поддерживается все то, что поддерживал MozSwing — win, linux, solaris, mac.

Ссылки проекта:
Проект JBrowser (stable)
Учебник

JWebPane

Надеюсь, он выйдет уже когда-нибудь и спасет мир. Очень жду этот проект.

JBrowser: реинкарнация MozSwing: 15 комментариев

  1. Добрый день!
    Если не сложно, можете ответить, какой самый нормальный способ выполнить javascript (заданный как строка) в JBrowser?

    • Добрый день! Извините не было времени вам ответить. Если без особых ухищрений – то создать html страничку и просто открыть ее в браузере тем самым выполнив скрипт.

  2. Добрый день. Как связать элемент полученый через DOM и реальный элемент прикрепленный к браузеру. Т.е. можно ли найти элемент страницы с помощью browser.getDocument() и выполнить на этом элементе некий event ?

    • Привет. Расскажите подробнее что нужно сделать, пока не очень понятно =)

      • С помощью browser.getDocument() я могу получить объектную модель загруженной страницы, в этой модели я могу найти нужные мне элементы страницы, но кроме информации о нужных мне элементах я ничего не могу получить, а я хочу производить какие то действия с элементами страницы , например перейти по ссылке или нажать кнопку.

      • Если я правильно понял — нужно программно активировать переход по ссылке (тут не вижу сложности, прочитал ссылку — установил как новый адрес для браузера), или нажимать на кнопку (выполнять произвольный JS скрипт?), или добавить/удалить элемент DOM модели.

        Пока занят, постараюсь завтра/послезавтра глянуть что можно сделать, думаю проблема решаема.

      • Насчет установки нового адреса для браузера, то тут немного не то, т.к. ссылка может обвешиваться яваскриптом. Я к тому что не могу найти связь между DOM моделью и управлением элементами.

      • Извините, занят был.
        Можно не хитрым способом делать следующее: редактировать DOM модель, выполнять javascript, делать callback’и в java. Вот пример:
            browser.setText("<html><body>" +
                "<div id='dynamic'>Hellow, JBrowser!</div><br/>" +
                "<button id='clickme' onclick='javascript: alert(1);'>Do it!</button><br/>" +
                "<button onclick="javascript: window.location = 'http://callback/WTF!!!';">Callback java</button><br/>" +
                "</body></html>");
            
            browser.addBrowserListener(new BrowserAdapter() {
              
              @Override
              public void onLoadingEnded() {
                nsIDOMDocument document = browser.getWebBrowser().getContentDOMWindow().getDocument();
                
                //
                // Добавить div:
                //
                nsIDOMElement dynamicDiv = document.getElementById("dynamic");
                nsIDOMElement createdDiv = document.createElement("div");
                createdDiv.appendChild(document.createTextNode("Новый div"));
                dynamicDiv.appendChild(createdDiv);
                
                //
                // Выполнить Javascript
                //
                browser.setUrl("javascript: document.getElementById('clickme').click();");
              }
              
              
              @Override
              public boolean beforeOpen(String uri) {
                // callback в яву (костыль)
                if (uri.startsWith("http://callback/")) {
                  System.out.println("Callback, uri: " + uri);
                  return false;
                }
                
                return super.beforeOpen(uri);
              }
              
            });

        Здесь используется средства jbrowser, а так же nsIDocument. Вообще это уже выходит за абстракцию jbrowser. Если вам не достаточно средств которые я привел — нужно курить мануалы по Gecko и запрашивать низкоуровневые интерфейсы через queryInterface (jbrowser позволяет это делать).

  3. Идея понятна, в принципе можно в страницу внедрить javascript, который будет делать все что нужно, это конечно не очень красивая модель , но если важен результат то подойдет. XPCOM это конечно жесть, не хочется даже это хозяйство трогать. За пример спасибо !

  4. Добрый день!
    Скажите, пожалуйста, как настроить JBrowser для соединения через прокси сервер требующий авторизации?

  5. В примере указываются хост и порт, а где указать логин и пароль для авторизации?

    • Хм. Действительно логин и пароль здесь не указываются. Хотя я помню что у меня все прекрасно работало с авторизацией. У меня два варианта:
      1. Браузер спросит логин и пароль, когда будет такая необходимость
      2. Возможно этот код поможет:

                  final String login = /*something*/;
                  final char[] password = /*something*/;
                  Authenticator.setDefault(new Authenticator() {
      
                      protected PasswordAuthentication getPasswordAuthentication() {
                          return new PasswordAuthentication(login, password);
                      }
                  });
      

      Попробуйте.

  6. К сожалению, оба варианта не работают. Может быть есть ещё способы?

    • Сходу не могу сказать. Время немножко освободится — подниму тестовое окружение и попробую разобраться. Что поражает в фаерфоксе я тоже не вижу полей для ввода логина и пароля.

Оставьте комментарий