воскресенье, 25 декабря 2011 г.

Интеграция Google Maps в GWT

     Google Maps API позволяет встраивать карты от Google в web-приложения при помощи JavaScript. Библиотека Google Maps for GWT предоставляет доступ к JavaScript API из Java кода скомпилированного GWT компилятором. 
     Рассмотрим использование Google Maps API и библиотеку для GWT. Создадим приложение на GWT, которое будет определять местоположение пользователя, выводить подробную информацию и показывать место на карте с маркером в центре. 
     Для работы с картами в classpath проекта нужно добавить библиотеку gwt-maps.jar, а также в конфигурационный файл MapGwtApp.gwt.xml дописать следующие строки:
<!-- Include Google Maps API bindings -->
<inherits name='com.google.gwt.maps.Maps' />

<script src="http://maps.google.com/maps/api/js?key=ABQIAAAAxp9dobcDAi8qyMyXsLdS6hT2yXp_ZAY8_ufC3CFXhHIE1NvwkxRiFg5rKqsubGL2ZNYXGXzEg7K5kA&amp;sensor=false" />


* This source code was highlighted with Source Code Highlighter.
     Создадим контейнер AnimatedMapWidget для карты, который позволит ей появляться/скрываться плавно с анимацией.
/**
* @author Dmitry Nikolaenko
*/
    
public class AnimatedMapWidget extends FlowPanel {

    /**
     * how long will take the panel to animate
     */
    private final int ANIMATION_DURATION = 500;

    public AnimatedMapWidget() {
        super();
    
        initWidgetStyle();
    
        show(false);
    
        this.setSize("300px", "250px");
    }
    
    private void initWidgetStyle() {
        this.getElement().getStyle().setPosition(Position.ABSOLUTE);
        this.getElement().getStyle().setBottom(0, Unit.PX);
        this.getElement().getStyle().setZIndex(-1);
        this.getElement().getStyle().setRight(0, Unit.PX);
    }

    @Override
    public void setVisible(final boolean visible) {
        AnimationEffect animationEffect = new AnimationEffect(visible);
    
        animationEffect.run(ANIMATION_DURATION);
    
        if (visible) {
            show(visible);
        } else {
            new Timer() {
                @Override
                public void run() {
                    show(visible);
                }
            }.schedule(ANIMATION_DURATION);
        }
    }

    /**
     * hide or show main panel without animation
     */
    public void show(boolean show) {
        super.setVisible(show);
    }

    /**
     * animation which will change property of the panel depending on the show value
     */
    private class AnimationEffect extends Animation {
    
        private boolean show = true;
    
        AnimationEffect(boolean show) {
            super();
        
            this.show = show;
        }

        @Override
        protected void onUpdate(double progress) {
             double rightValue = progress;
            
             if (!show) {
                 rightValue = 1.0 - progress;
             }
            
             AnimatedMapWidget.this.getElement().getStyle().setRight(-AnimatedMapWidget.this.getOffsetWidth() * rightValue, Unit.PX);
        }
    }
}


* This source code was highlighted with Source Code Highlighter.
     Давайте опишем с помощью enum GoogleMapZoomType тип увеличения для карты:
/**
* @author Dmitry Nikolaenko
*/

public enum GoogleMapZoomType {
    COUNTRY_ZOOM(5),
    REGION_ZOOM(9),
    TOWN_ZOOM(13),
    ADDRESS_ZOOM(17);

    private int zoom;

    private GoogleMapZoomType(int zoom) {
        this.zoom = zoom;
    }

    public int getZoomLevel() {
        return zoom;
    }
}


* This source code was highlighted with Source Code Highlighter.
  Инициализация карты и ее свойств происходит в методах initGoogleMapsWidget() и getGoogleMapsWidget(). Для отображения локации по текущим географическим координатам и установки маркера на карте используются методы displayLocationOnMap() и displayLocationMarker().
/**
* display the current location on the map
* @param geoCoord the geographical coordinates, latitude and longitude
* @param zoomType the zoom level for the map
* @param addressLocation the new address location
*/
private void displayLocationOnMap(HasLatLng geoCoord, GoogleMapZoomType zoomType, String addressLocation) {
    geoRequest.setLatLng(geoCoord);
    geoRequest.setAddress(addressLocation);
    displayLocationMarker(geoRequest, zoomType);
}


* This source code was highlighted with Source Code Highlighter.
/**
* show the marker on the map
*/
public static void displayLocationMarker(final GeocoderRequest request, final GoogleMapZoomType zoomType) {
    new Geocoder().geocode(request, new GeocoderCallback() {
        @Override
        public void callback(List<HasGeocoderResult> responses, String status) {
            if (status.equalsIgnoreCase("ok")) {
                final HasGeocoderResult geoResult = responses.get(0);
                final HasLatLng geoLatLng = geoResult.getGeometry().getLocation();
            
                StringBuilder sb = new StringBuilder();
                for (HasAddressComponent component : geoResult.getAddressComponents()) {
                     sb.append(component.getLongName()).append(", ");
                }
                addressLocationInfo.setText(sb.toString());
            
                mapMarker.setPosition(geoLatLng);
                mapMarker.setDraggable(true);
                mapMarker.setMap(mapWidget.getMap());
            
                mapWidget.getMap().setZoom(zoomType.getZoomLevel());
                mapWidget.getMap().panTo(geoLatLng);
                mapMarker.setMap(mapWidget.getMap());
            } else {
                GWT.log("Geocoder failed with response : " + status);
            }
        }
    });
}


* This source code was highlighted with Source Code Highlighter.
     С помощью JSNI в методе detectCurrentLocation() определяем текущее положение(долготу и ширину).
/**
* JSNI method, try detect current location
*/
private static native void detectCurrentLocation() /*-{
    if ($wnd.navigator.geolocation) {
        $wnd.navigator.geolocation.getCurrentPosition(function(position) {
             var lat = position.coords.latitude;
             var lon = position.coords.longitude;
             @com.dmitrynikol.map.gwt.client.MapGwtApp::currentLocationDetected(DD)(lat, lon);
        }, function() {
             // could not find location
        });
    }
}-*/;


* This source code was highlighted with Source Code Highlighter.
     А если у браузера есть поддержка HTML 5, то для нахождения текущих координат можно воспользоваться методом findCurrentLocation().
/**
* another way to find current location
*/
private void findCurrentLocation() {
    Geolocation.getIfSupported().getCurrentPosition(new Callback<Position, PositionError>() {
        @Override
        public void onSuccess(Position result) {
            double latitide = result.getCoordinates().getLatitude();
            double longitude = result.getCoordinates().getLongitude();
            GWT.log(latitide + " / " + longitude);
        }
    
        @Override
        public void onFailure(PositionError reason) {
            GWT.log(reason.getLocalizedMessage());
        }
    });
}


* This source code was highlighted with Source Code Highlighter.
     Метод refreshGoogleMapsWidget() позволяет возбудить resize событие, обновить и отцентрировать карту.
/**
* refresh google maps widget and preserves the center point
*/
public void refreshGoogleMapsWidget() {
    if (mapWidget != null) {
        HasLatLng center = mapWidget.getMap().getCenter();
        Event.trigger(mapWidget.getMap(), "resize");
        mapWidget.getMap().setCenter(center);
    }
}


* This source code was highlighted with Source Code Highlighter.
     Полный код класса MapGwtApp, где и будет стартовать приложение:
/**
* @author Dmitry Nikolaenko
*/

public class MapGwtApp implements EntryPoint {

    private FlowPanel mainPanel;
    private AnimatedMapWidget animatedMapWidget;
    private Button show;
    private static Label addressLocationInfo;

    private static MapWidget mapWidget;
    private MapOptions options;
    private static GeocoderRequest geoRequest;
    private static Marker mapMarker;

    private boolean showMap = false;

    @Override
    public void onModuleLoad() {
        mainPanel = new FlowPanel();
        animatedMapWidget = new AnimatedMapWidget();
        show = new Button("show map");
        addressLocationInfo = new Label();
    
        show.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                showMap = !showMap;
                show.setText(showMap ? "hide map" : "show map");
                animatedMapWidget.setVisible(showMap);
            
                if (showMap) {
                    refreshGoogleMapsWidget();
                }
            }
        });
    
        mainPanel.setStyleName("mainPanelStyle");
        mainPanel.add(show);
        mainPanel.add(addressLocationInfo);
        mainPanel.add(animatedMapWidget);
        RootPanel.get().add(mainPanel);
    
        initGoogleMapsWidget();
        detectCurrentLocation();
    }

    private void initGoogleMapsWidget() {
        geoRequest = new GeocoderRequest();
        mapMarker = new Marker();
        options = new MapOptions();
    
        getGoogleMapsWidget();
        displayLocationOnMap(options.getCenter(), GoogleMapZoomType.REGION_ZOOM, "");
        animatedMapWidget.add(mapWidget);
    }

    /**
     * create map widget that presents a viewable Google Map to a user
     */
    public void getGoogleMapsWidget() {
        options.setCenter(new LatLng(39.509, -98.434));
    
        options.setDraggable(true);
        options.setNavigationControl(true);
    
        options.setMapTypeId(new MapTypeId().getRoadmap());
        options.setMapTypeControl(true);
        options.setScrollwheel(true);
    
        mapWidget = new MapWidget(options);
        mapWidget.setSize("100%", "100%");
    }

    /**
     * refresh google maps widget and preserves the center point
     */
    public void refreshGoogleMapsWidget() {
        if (mapWidget != null) {
            HasLatLng center = mapWidget.getMap().getCenter();
            Event.trigger(mapWidget.getMap(), "resize");
            mapWidget.getMap().setCenter(center);
        }
    }

    /**
     * display the current location on the map
     * @param geoCoord the geographical coordinates, latitude and longitude
     * @param zoomType the zoom level for the map
     * @param addressLocation the new address location
     */
    private void displayLocationOnMap(HasLatLng geoCoord, GoogleMapZoomType zoomType, String addressLocation) {
        geoRequest.setLatLng(geoCoord);
        geoRequest.setAddress(addressLocation);
        displayLocationMarker(geoRequest, zoomType);
    }

    /**
     * another way to find current location
     */
    private void findCurrentLocation() {
        Geolocation.getIfSupported().getCurrentPosition(new Callback<Position, PositionError>() {
            @Override
            public void onSuccess(Position result) {
                double latitide = result.getCoordinates().getLatitude();
                double longitude = result.getCoordinates().getLongitude();
                GWT.log(latitide + " / " + longitude);
            }
        
            @Override
            public void onFailure(PositionError reason) {
                GWT.log(reason.getLocalizedMessage());
            }
        });
    }

    /**
     * JSNI method, try detect current location
     */
    private static native void detectCurrentLocation() /*-{
        if ($wnd.navigator.geolocation) {
            $wnd.navigator.geolocation.getCurrentPosition(function(position) {
                 var lat = position.coords.latitude;
                 var lon = position.coords.longitude;
                 @com.dmitrynikol.map.gwt.client.MapGwtApp::currentLocationDetected(DD)(lat, lon);
            }, function() {
                 // could not find location
            });
        }
    }-*/;

    public static void currentLocationDetected(double latitude, double longitude) {
        geoRequest.setLatLng(new LatLng(latitude, longitude));
    
        displayLocationMarker(geoRequest, GoogleMapZoomType.REGION_ZOOM);
    }

    /**
     * show the marker on the map
     */
    public static void displayLocationMarker(final GeocoderRequest request, final GoogleMapZoomType zoomType) {
        new Geocoder().geocode(request, new GeocoderCallback() {
            @Override
            public void callback(List<HasGeocoderResult> responses, String status) {
                if (status.equalsIgnoreCase("ok")) {
                    final HasGeocoderResult geoResult = responses.get(0);
                    final HasLatLng geoLatLng = geoResult.getGeometry().getLocation();
                
                    StringBuilder sb = new StringBuilder();
                    for (HasAddressComponent component : geoResult.getAddressComponents()) {
                         sb.append(component.getLongName()).append(", ");
                    }
                    addressLocationInfo.setText(sb.toString());
                
                    mapMarker.setPosition(geoLatLng);
                    mapMarker.setDraggable(true);
                    mapMarker.setMap(mapWidget.getMap());
                
                    mapWidget.getMap().setZoom(zoomType.getZoomLevel());
                    mapWidget.getMap().panTo(geoLatLng);
                    mapMarker.setMap(mapWidget.getMap());
                } else {
                    GWT.log("Geocoder failed with response : " + status);
                }
            }
        });
    }
}


* This source code was highlighted with Source Code Highlighter.
     Вот так выглядит запущенное приложение:

воскресенье, 27 ноября 2011 г.

Анимация в GWT

    Начиная с версии GWT 2.0 система лейаутов имеет прямую поддержку анимации. Это необходимо для поддержки ряда сценариев использования, потому что система лейаутов должна надлежащим образом поддерживать обработку анимации среди различных ее наборов. Панели которые реализуют интерфейс AnimatedLayout, напр. LayoutPanel, DockLayoutPanel, SplitLayoutPanel, могут анимировать их дочерние виджеты из одного набора значений к другому. Обычно это делается путем создания отношений, которые можно оживить путем вызова метода animate()
     Анимация представляет собой полезный инструмент для изменения свойств виджета в непрерывном движении. С ее помощью можно получить более дружественный интерфейс, при правильном использовании ;) Анимацию использовать достаточно легко, нужно унаследоваться от класса Animation и переопределить метод onUpdate()
  Метод onUpdate(double progress) - обновляется каждые 25 миллисекунд, задано это через Animation.DEFAULT_FRAME_DELAY, значение перемен. progress меняется от 0 до 1, по этому каждые 25 миллисекунд мы получим немножко измененное значение progress. После создания экземпляра AnimationEffect мы запускаем анимацию через вызов метода  run(int duration). Продолжительность является длительностью анимации в миллисекундах. 
     Также можно переопределить другие полезные методы класса Animation:
onStart() - вызывается перед началом анимации
onComplete() - вызывается после окончания анимации
onCancel() - вызывается после отмены анимации
     Давайте рассмотрим на примерах как это все работает.
Пример 1: 
     Создадим анимированнyю FlowPanel, где появление и скрытие будет сопровождаться плавным переходом изменения прозрачности, длительность перехода(анимации) можно изменять через переменную.
/**
* @author Dmitry Nikolaenko
*/
public class AnimatedFlowPanel extends FlowPanel {

    /**
     * how long will take the panel to animate
     */
    private final int ANIMATION_DURATION = 1000;
 
    public AnimatedFlowPanel() {
        super();
    }

    @Override
    public void setVisible(final boolean visible) {
        AnimationEffect animationEffect = new AnimationEffect(visible);
        // run animation
        animationEffect.run(ANIMATION_DURATION);
        // when animation will end, the panel must be hidden
        Timer timer = new Timer() {
            @Override
            public void run() {
                AnimatedFlowPanel.this.setVisible(visible);
            }
        };
    }

    /**
     * animation which will change opacity of the panel depending on the show value,
     * false is disappear, true is appear
     */
    private class AnimationEffect extends Animation {
 
        private boolean show = true;
 
        AnimationEffect(boolean show) {
            super();
            this.show = show;
        }

        @Override
        protected void onUpdate(double progress) {
            double opacityValue = progress;
     
            if (!show) {
                opacityValue = 1.0 - progress;
            }
     
            AnimatedFlowPanel.this.getElement().getStyle().setOpacity(opacityValue);
        }
    }
}


* This source code was highlighted with Source Code Highlighter.
Пример 2:
      Анимированная PopupPanel, продолжительность появления и затухания всплывающей панели можно задавать вручную.
public class AnimatedPopupPanel extends PopupPanel {

    /**
     * decides if panel should hide after some time or not
     */
    private boolean shouldHide = true;

    /**
     * how long will take the panel to animate
     */
    private final int ANIMATION_DURATION = 2000;

    public AnimatedPopupPanel(boolean shouldHide) {
        super(true);
        this.shouldHide = shouldHide;
        setAutoHideEnabled(true);
        setAnimationEnabled(true);
        addStyleName("customPopupPanel");
    }

    @Override
    public void show() {
        PopupPanelAnimation showAnimation = new PopupPanelAnimation();
        showAnimation.run(ANIMATION_DURATION);
        super.show();
 
        if (shouldHide) {
            PopupPanelAnimation hideAnimation = new PopupPanelAnimation(false);
            // run hide animation after some time
            hideAnimation.run(ANIMATION_DURATION, Duration.currentTimeMillis() + ANIMATION_DURATION);
            // when animation will end, the widget must be hidden
            Timer timer = new Timer() {
                @Override
                public void run() {
                    AnimatedPopupPanel.this.hide();
                }
            };
        }
    }

    /**
     * animation which will change opacity of the widget depending on the show value,
     * false is disappear, true is appear
     */
    private class PopupPanelAnimation extends Animation {
        boolean show = true;
 
        PopupPanelAnimation(boolean show) {
            super();
            this.show = show;
        }
 
        public PopupPanelAnimation() {
            this(true);
        }

        @Override
        protected void onUpdate(double progress) {
            double opacityValue = progress;
     
            if (!show) {
                opacityValue = 1.0 - progress;
            }
     
            AnimatedPopupPanel.this.getElement().getStyle().setOpacity(opacityValue);
        }
    }
}


* This source code was highlighted with Source Code Highlighter.
Пример 3: 
     Летающая/плавающая панель (FlyingPanel), которая меняет свою позицию в зависимости от выбранного контента (статьи или комментарии). Пример такой панели можно посмотреть на http://www.membrana.ru открыв любую статью с комментариями. Переход на панели article/comments осуществляется с помощью движения мышки, просто нужно переместить курсор на одну из панелей, и она сразу поменяет свое положение для более удобного чтения. Длительность анимации и поведение свойств панелей можно задавать. В примере меняется только положение панелей.
/**
* @author Dmitry Nikolaenko
*/
public class FlyingPanel extends FlowPanel {

    public enum Toggle {
        ARTICLE,
        COMMENTS;
    }

    /**
     * how long will take the panel to animate
     */
    private final int ANIMATION_DURATION = 500;

    private final int DATA_TOGGLE_WIDTH = 200;
    private final int DATA_TOGGLE_HEIGHT = 100;

    /**
     * left css property of the main panel
     */
    private double leftProperty = 0.0;

    private Label article;
    private Label comments;

    public FlyingPanel() {
        super();
        this.getElement().getStyle().setPosition(Position.RELATIVE);
        this.getElement().getStyle().setWidth(420, Unit.PX);
 
        article = new Label("article");
        comments = new Label("comments");
 
        article.addMouseOverHandler(new FlyingMouseOverHandler(Toggle.ARTICLE));
        comments.addMouseOverHandler(new FlyingMouseOverHandler(Toggle.COMMENTS));
 
        initToggleStyle();
 
        add(article);
        add(comments);
    }

    private void initToggleStyle() {
        article.getElement().getStyle().setWidth(DATA_TOGGLE_WIDTH, Unit.PX);
        comments.getElement().getStyle().setWidth(DATA_TOGGLE_WIDTH, Unit.PX);
 
        article.getElement().getStyle().setHeight(DATA_TOGGLE_HEIGHT, Unit.PX);
        comments.getElement().getStyle().setHeight(DATA_TOGGLE_HEIGHT, Unit.PX);
 
        article.getElement().getStyle().setBorderStyle(BorderStyle.SOLID);
        comments.getElement().getStyle().setBorderStyle(BorderStyle.SOLID);
 
        article.getElement().getStyle().setFloat(Float.LEFT);
        comments.getElement().getStyle().setFloat(Float.LEFT);
    }

    /**
     * mouse over handler for both side of the main panel
     */
    private class FlyingMouseOverHandler implements MouseOverHandler {
 
        private Toggle toggle;
 
        private FlyingMouseOverHandler(Toggle toggle) {
            this.toggle = toggle;
        }
 
        @Override
        public void onMouseOver(MouseOverEvent event) {
            setSelected(toggle);
        }
    }

    public void setSelected(Toggle toggle) {
        if (toggle.equals(Toggle.ARTICLE) || leftProperty >= -1) {
            AnimationEffect animationEffect = new AnimationEffect(toggle);
     
            // run animation
            animationEffect.run(ANIMATION_DURATION);
        }
    }

    /**
     * animation which will change left property of the panel depending on the toggle value
     */
    private class AnimationEffect extends Animation {
 
        private Toggle toggle = Toggle.ARTICLE;
 
        AnimationEffect(Toggle toggle) {
            super();
            this.toggle = toggle;
        }

        @Override
        protected void onUpdate(double progress) {
            double leftValue = progress;
     
            boolean fly = toggle.equals(Toggle.COMMENTS);
     
            if (!fly) {
                leftValue = 1.0 - progress;
            }
     
            leftProperty = (fly ? (-DATA_TOGGLE_WIDTH / 2) : leftProperty) * leftValue;
     
            FlyingPanel.this.getElement().getStyle().setLeft(leftProperty, Unit.PX);
        }
    }
}


* This source code was highlighted with Source Code Highlighter.

пятница, 25 ноября 2011 г.

Использование ClientBundle в GWT

     Ресурсы в развернутых GWT приложениях могут быть условно разделены на: некешируемые (.nocache.js), которые всегда кешируются (.cache.html) и “все остальное” (app.css). Интерфейс ClientBundle перемещает записи из категории “все остальное” в всегда кешируемую категорию. 
     ClientBundle - это механизм в GWT для предотвращения медленного выполнения клиентского приложения, он кеширует различные виды ресурсов, напр. изображения, CSS, тестовые и т.д. Преимущество подхода заключается в том, что необходимый ресурс будет загружен один раз, вместо подтягивания каждый раз во время использования. Также уменьшится общий объем хранимых ресурсов. В результате будет уменьшено расходы для хранения однотипных данных. 
     Пример:
Создадим два интерфейса наследуемые от ClientBundle с методами для доступа к изображениям.
public interface FirstBundle extends ClientBundle{
    @Source("first-icon.png")
    ImageResource red();
}

public interface SecondBundle extends ClientBundle {
    @Source("second-icon.png")
    ImageResource blue();
}


* This source code was highlighted with Source Code Highlighter.
     В используемом виджете с помощью аннотации @Inject в Gin, говорим ему использовать этот конструктор. У Gin-а есть специальная обработка для GWT deferred binding, которая потребует минимальной настройки для работы. Т.к. нам не нужно дополнительное конфигурирование, то метод configure() класса ClientBundleModule останется пустым.
@Inject
public MainWidget(FirstBundle resourcesFirst, SecondBundle resourcesSecond) {
    initWidget(uiBinder.createAndBindUi(this));
    
    leftLabel.add(new Image(resourcesFirst.red()));
    rightLabel.add(new Image(resourcesSecond.blue()));
}

@GinModules(ClientBundleModule.class)
public interface ClientBundleInjector extends Ginjector {
    FirstBundle firstBundle();
    SecondBundle secondBundle();
}

public class ClientBundleModule extends AbstractGinModule {
    @Override
    protected void configure() {
    }
}


* This source code was highlighted with Source Code Highlighter.
      После выполнения кода через GWT компилятор на выходе мы получим war-архив с множеством png файлов, среди которых будут first-icon.png и second-icon.png, как отдельные файлы. Давайте объединим наши изображения в одно, тем самым уменьшим загрузку.
public interface ClientBundleExample extends FirstBundle, SecondBundle {
}

public class ClientBundleModule extends AbstractGinModule {
    @Override
    protected void configure() {
        bind(FirstBundle.class).to(ClientBundleExample.class);
        bind(SecondBundle.class).to(ClientBundleExample.class);
    }
}
.....
private final ClientBundleInjector injector = GWT.create(ClientBundleInjector.class);

@Inject
public MainWidget() {
    initWidget(uiBinder.createAndBindUi(this));
    
    leftLabel.add(new Image(injector.firstBundle().red()));
    rightLabel.add(new Image(injector.secondBundle().blue()));
}


* This source code was highlighted with Source Code Highlighter.
Теперь после компиляции мы получим один png файл содержащий два изображения.

понедельник, 31 октября 2011 г.

Тестирование GWT с помощью Mockito

    Иногда в процессе разработки возникает необходимость использования интерфейса, до того, как он будет реализован. Тогда нам на помощь приходят mock-обьекты, которые предоставляют фиктивную реализацию интерфейса. Они заменяют реальный объект во время теста и позволяют имитировать поведения.
      Mockito - это очень интересный фреймворк для mock-тестирования, который позволяет писать красивые и понятные тесты. Про основные возможности и особенности можно прочитать по след. ссылке. В процессе тестирования мы будем использовать класс GWTMockUtilities, который позволит имитировать классы GWT используя Mockito как в JUnit тестах. Что же произойдет если мы не будет использовать этот класс? При запуске теста мы получим след. ошибку:
java.lang.ExceptionInInitializerError
    at sun.reflect.GeneratedSerializationConstructorAccessor1.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:40)
    at org.objenesis.ObjenesisBase.newInstance(ObjenesisBase.java:59)
    at org.mockito.internal.creation.jmock.ClassImposterizer.createProxy(ClassImposterizer.java:111)
    …...
Caused by: java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in client code! It cannot be called, for example, from server code. If you are running a unit test, check that your test case extends GWTTestCase and that GWT.create() is not called from within an initializer or constructor.
    at com.google.gwt.core.client.GWT.create(GWT.java:92)
    at com.google.gwt.user.client.ui.UIObject.<clinit>(UIObject.java:188)
    ... 35 more


* This source code was highlighted with Source Code Highlighter.
     Метод GWT.create() вызывается при инициалиализации статического поля UiObject, это происходит, когда JVM загружает класс TextBox. Сообщение об исключении рекомендует чтобы тест наследовался от GWTTestCase, случай интеграционного теста, который запускает JavaScript в хостинг режиме браузера. Но в нашем тесте мы не используем настоящее текстовое поле, мы просто хотим создать копию. Так почему же мы должны тратить время при выполнении теста в медленном GWTTestCase? 
     GWTMockUtilities предоставляет способ решения этой проблемы. Через обертывание тестовых методов с GWTMockUtilities.disarm() и restore(), мы можем временно отключить побочный эффект вызова GWT.create() при статической инициализации. 
     Пример
у нас есть определенная view - ReceiverWidget с текстовым полем и методами для установки и получения значения
public class ReceiverWidget extends Composite implements IReceiverWidget {
    private static ReceiverWidgetUiBinder uiBinder = GWT.create(ReceiverWidgetUiBinder.class);

    @UiTemplate("ReceiverWidget.ui.xml")
    interface ReceiverWidgetUiBinder extends UiBinder<Widget, ReceiverWidget> {
    }

    @UiField TextBox description;
    // other fields

    public ReceiverWidget() {
        initWidget(uiBinder.createAndBindUi(this));
    }

    public void setDescription(String text) {
        description.setText(text);
    }

    public String getDescription() {
        return description.getText();
    }

    // other methods
}


* This source code was highlighted with Source Code Highlighter.
и виджет GameBox у которого есть кнопка, но которую можно навешать ClickHandler
public class GameBox extends Composite {

    @UiTemplate("GameBox.ui.xml")
    interface GameBoxUiBinder extends UiBinder<Widget, GameBox>{}

    public static GameBoxUiBinder uiBinder = GWT.create(GameBoxUiBinder.class);

    @UiField Button ok;
    // other fields    

    public GameBox() {
        this.initWidget(uiBinder.createAndBindUi(this));
    }

    public HasClickHandlers getOkButton() {
        return ok;
    }

    public void showWindowAlert() {
        Window.alert("Mockito");
    }

    // other methods
}


* This source code was highlighted with Source Code Highlighter.
     Протестируем класс ReceiverWidget
public class ReceiverWidgetTest {
    private ReceiverWidget receiverWidget;

    @Before
    public void disarm() {
        GWTMockUtilities.disarm();
        receiverWidget = mock(ReceiverWidget.class, CALLS_REAL_METHODS);
   
        receiverWidget.description = mock(TextBox.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS));
    }

    @After
    public void restore() {
        GWTMockUtilities.restore();
    }

    @Test
    public void testSetDescription() {
        String testValue = "test";
   
        receiverWidget.setDescription(testValue);
        verify(receiverWidget.description).setText(testValue);
    }

    @Test
    public void testGetDescription() {
        String testValue = "test";
   
        Mockito.when(receiverWidget.description.getText()).thenReturn(testValue);
        String value = receiverWidget.description.getText();
        verify(receiverWidget.description).getText();
        assertEquals(testValue, value);
    }
}


* This source code was highlighted with Source Code Highlighter.
     Класс GameBox используется в ReceiverPresenter презентере, мы просто навешиваем обработчик ClickHandler на его кнопку
public class ReceiverPresenter extends AbstractGroupPresenter<IReceiverWidget, AppEventBus> {

    public interface IReceiverWidget {
        void showGroup(String group);
        void setReceivedValue(String value);
    }

    GameBox gameBox;

    @Override
    public void bind() {
        view.showGroup(getGame());
   
        gameBox.getOkButton().addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                gameBox.showWindowAlert();
            }
        });

        // …
    }

    // other methods
}


* This source code was highlighted with Source Code Highlighter.
     Давайте напишем класс ReceiverPresenterTest который протестирует наш презентер
public class ReceiverPresenterTest {

    private ReceiverPresenter presenter;
    private AppEventBus eventBus;
    private IReceiverWidget view;

    /**
     * method to simulate clicking
     *
     * @return
     */
    public static Answer<?> createClickEmulator() {
        Answer<?> clickEmulator = new Answer<Object>() {
            public Object answer(InvocationOnMock invocation) throws Throwable {
                ClickHandler clickHandler = (ClickHandler) invocation.getArguments()[0];
                clickHandler.onClick(null);
                return null;
            }
        };
        return clickEmulator;
    }

    @Before
    public void disarm() {
        GWTMockUtilities.disarm();
   
        presenter = mock(ReceiverPresenter.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));

        eventBus = mock(AppEventBus.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
        presenter.setEventBus(eventBus);
        view = mock(IReceiverWidget.class, RETURNS_DEEP_STUBS);
        presenter.setView(view);
        presenter.gameBox = mock(GameBox.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS));
    }

    @After
    public void restore() {
        GWTMockUtilities.restore();
    }

    @Test
    public void testBind() {
        presenter.bind();

        verify(presenter.gameBox.getOkButton()).addClickHandler((ClickHandler) any());
    }

    @Test
    public void testBindOkButtonClick() {
        when(presenter.gameBox.getOkButton().addClickHandler((ClickHandler) any())).thenAnswer(createClickEmulator());
   
        presenter.bind();
   
        verify(presenter.gameBox).showWindowAlert();
    }
}


* This source code was highlighted with Source Code Highlighter.
Вот и все. Как видите нет ничего сложного, тесты проходят успешно, как мы и ожидали.

вторник, 18 октября 2011 г.

GWT & Mvp4G Event Filter

    В продолжение статьи об GWT + Mvp4G рассмотрим включение/отключение обработчиков с помощью Mvp4G Event Filter. Mvp4g позволяет фильтровать события и останавливать до того как они будут переданы обработчикам. Избавимся от условий if на каждое событие в группах презентеров. Сначала создадим PresenterHandler, который будет отвечать за инициализацию презентеров, добавление их в группы отправителей/получателей и активации списка презентеров для группы событий.
@EventHandler
@Singleton
public class PresenterHandler extends BaseEventHandler<AppEventBus> {

    private Map<Game, List<AbstractGroupPresenter<?, AppEventBus>>> presenterMap;

    public PresenterHandler() {
        presenterMap = new HashMap<Game, List<AbstractGroupPresenter<?, AppEventBus>>>();
    }

    public void onInit() {
        addWidget(SenderPresenter.class, Game.Action);
        addWidget(ReceiverPresenter.class, Game.Action);
        addWidget(ReceiverPresenter.class, Game.Action);
        addWidget(ReceiverPresenter.class, Game.Action);

        addWidget(SenderPresenter.class, Game.RPG);
        addWidget(ReceiverPresenter.class, Game.RPG);
        addWidget(ReceiverPresenter.class, Game.RPG);
        addWidget(ReceiverPresenter.class, Game.RPG);

        addWidget(SenderPresenter.class, Game.Strategy);
        addWidget(ReceiverPresenter.class, Game.Strategy);
        addWidget(ReceiverPresenter.class, Game.Strategy);
        addWidget(ReceiverPresenter.class, Game.Strategy);
    }

    private void addWidget(
            Class<? extends AbstractGroupPresenter<?, AppEventBus>> component, Game game) {
        AbstractGroupPresenter<?, AppEventBus> presenter = (AbstractGroupPresenter<?, AppEventBus>) eventBus
                .addHandler(component, false);
        presenter.setGame(game.toString());
        presenter.bind();
        if (!presenterMap.containsKey(game)) {
            presenterMap.put(game, new ArrayList<AbstractGroupPresenter<?, AppEventBus>>());
        }
        presenterMap.get(game).add(presenter);
    
        if (presenter instanceof SenderPresenter) {
            eventBus.showSenderWidget((Widget) presenter.getView());
        } else {
            eventBus.showReceiverWidget((Widget) presenter.getView());
        }
    }

    /**
     * activate the presenter list for an event group
     */
    public void activateGroup(Game groupToActivate) {
        for (Game game : presenterMap.keySet()) {
            for (AbstractGroupPresenter<?, AppEventBus> presenter : presenterMap.get(game)) {
                // when activating an empty group, activate all presenter
                if (groupToActivate == null || groupToActivate.equals("") || groupToActivate.equals(game)) {
                    presenter.setActivated(true);
                } else {
                    presenter.setActivated(false);
                }
            }
        }
    }
}


* This source code was highlighted with Source Code Highlighter.
     Далее создадим фильтр (AppPresentersEventFilter), который будет срабатывать на каждое событие вызванное в нашей шине событий (AppEventBus).
public class AppPresentersEventFilter implements EventFilter<AppEventBus> {

    @Inject
    private PresenterHandler presenterHandler;

    @Override
    public boolean filterEvent(String eventName, Object[] params, AppEventBus eventBus) {
        // deactivate presenter registered for a group when the event name ends in "$"
        if (eventName.endsWith("$")) {
            presenterHandler.activateGroup(Game.valueOf((String) params[0]));
        }
    
        // always return true to forward event to all active handlers
        return true;
    }
}


* This source code was highlighted with Source Code Highlighter.
     И подключим к шине событий:
@Filters(filterClasses = AppPresentersEventFilter.class)
@Events(startView = MainWidget.class)
public interface AppEventBus extends EventBus {

    @Start
    @Event(handlers = { PresenterHandler.class })                        
    public void init();

    @Event(handlers = { MainPresenter.class })
    public void showSenderWidget(IsWidget widget);

    @Event(handlers = { MainPresenter.class })
    public void showReceiverWidget(IsWidget widget);

    @Event(handlers = {ReceiverPresenter.class})
    public void setSelectedItem$(String genre, String value);
}


* This source code was highlighted with Source Code Highlighter.
     Также можно динамически добавлять/удалять фильтр с помощью методов addEventFilter и removeEventFilter в шине событий.
AppPresentersEventFilter filter = new AppPresentersEventFilter();
eventBus.addEventFilter(filter);
eventFilter.removeEventFilter(filter);


* This source code was highlighted with Source Code Highlighter.
     В шине используется метод setSelectedItem$(String genre, String value) с особым символом для фильтрации событий. Когда происходит событие и срабатывает фильтр, мы делаем проверку окончания имени события. Если имя заканчивается на символ “$”, то отключаем зарегистрированную группу презентеров. В фильтре происходит инжект PresenterHandler, через который вызываем метод activateGroup(Game groupToActivate) для включения/отключения списка презентеров для группы событий.
     Презентеры (Main, Receiver and Sender) после изменений выглядят вот так:
@Presenter(view = MainWidget.class)
public class MainPresenter extends BasePresenter<IMainWidget, AppEventBus> {

    public interface IMainWidget {
        public void addSenderWidget(IsWidget widget);
        public void addReceiverWidget(IsWidget widget);
    }

    public void onShowSenderWidget(IsWidget widget) {
        view.addSenderWidget(widget);
    }

    public void onShowReceiverWidget(IsWidget widget) {
        view.addReceiverWidget(widget);
    }
}


* This source code was highlighted with Source Code Highlighter.
@Presenter(view = ReceiverWidget.class, multiple = true)
public class ReceiverPresenter extends AbstractGroupPresenter<IReceiverWidget, AppEventBus> {

    public interface IReceiverWidget {
        void showGroup(String group);
        void setReceivedValue(String value);
    }

    @Override
    public void bind() {
        view.showGroup(getGame());
    }

    public void onSetSelectedItem$(String genre, String value) {
        List<String> games = Genre.valueOf(value).getGames();
        view.setReceivedValue(games.get(Random.nextInt(games.size())));
    }
}


* This source code was highlighted with Source Code Highlighter.
@Presenter(view = SenderWidget.class, multiple = true)
public class SenderPresenter extends AbstractGroupPresenter<ISenderWidget, AppEventBus> {
    public interface ISenderWidget {
        void showGroup(String group);
        HasChangeHandlers getSelectableComponent();
        String getSelectedValue();
        void displayValues(List<Genre> asList);
    }

    public void bind() {
        view.showGroup(getGame());
        view.getSelectableComponent().addChangeHandler(new ChangeHandler() {
            @Override
            public void onChange(ChangeEvent event) {
                eventBus.setSelectedItem$(getGame(), view.getSelectedValue());
            }
        });
        view.displayValues(Game.valueOf(getGame()).getGenre());
    }
}


* This source code was highlighted with Source Code Highlighter.
     Вот и все. Eclipse проект можно скачать по следующей ссылке.