Thursday, June 26, 2014

Spring MVC - Thymeleaf - Bootstrap - Twitter Flight - II.2

Source Code | See Application
You can see the full description of the aplication and the other two entries related here

Initial configuration

The web.xml will be the same as the shown previously.

Spring Web Application Context - twitterFlightExample-servlet.xml

For default Spring search the webapplication file with the pattern: [servlet-name]-servlet.xml in this case the file will be: twitterFlightExample-servlet.xml
<beans>
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>

<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="favorPathExtension" value="false"/>
    <property name="favorParameter" value="true"/>
    <property name="mediaTypes">
        <value>
            json=application/json
        </value>
    </property>
</bean>

<import resource="classpath*:META-INF/spring/applicationContext*.xml"/>

<context:component-scan base-package="org.anotes.example.twitterflight.*"/>

</beans>

Notes

  • Thymeleaf is not needed because all will be rendered using js templates.
  • It is needed to use the contentNegotiationManager in order to set the favorPathExtension to false so the ajax call (request ...htm) can return json objects. (Note: favorPathExtension is true by default so we would have error code 406 as response)

Controller

We will use the following controller:
@Controller
public class Main2Controller {

    @Autowired
    ProductService productService;
    @Autowired
    MainInfo mainInfo;

    @RequestMapping("/main2")
    public ModelAndView main() {
        return new ModelAndView("main2");
    }

    @RequestMapping(value = "/changePage2", method = RequestMethod.POST)
    @ResponseBody
    public Main2Json changePage(@RequestParam("page") Integer page) {
        logger.info("Changing page to:{}", page);
        mainInfo.setCurrentPage(page);
        List<Product> products = productService.findProducts(mainInfo.getFilter(), mainInfo.getPageInfo());
        Main2Json main2Json = new Main2Json();
        main2Json.setProductList(products);
        main2Json.setPageInfo(mainInfo.getPageInfo());
        return main2Json;
    }

    @RequestMapping(value = "/changeFilter2", method = RequestMethod.POST)
    @ResponseBody
    public Main2Json changeFilter(@RequestParam("filter") String filter, @RequestParam("value") String value) {
        logger.info("Changing filter to:({}:{})", filter, value);
        mainInfo.updateFilter(filter, value);
        SummaryInfo summaryInfo = productService.createSummaryInfoFor(mainInfo.getFilter());
        mainInfo.setSummaryInfo(summaryInfo);
        List<Product> products = productService.findProducts(mainInfo.getFilter(), mainInfo.getPageInfo());
        Main2Json main2Json = new Main2Json();
        main2Json.setTotalPages(mainInfo.getPageInfo().getTotalPages());
        main2Json.setBrandSummaryList(summaryInfo.getBrandSummaryList());
        main2Json.setProductList(products);
        main2Json.setPageInfo(mainInfo.getPageInfo());
        main2Json.setFilterEntryList(mainInfo.getFilter().getFilterEntryList());
        return main2Json;
    }

Notes:

  • main() It renders the main page; in this case this page only contains html code so none model needs to be sent.
  • changePage(@RequestParam("page") Integer page) It changes the page to the value sent as parameter, get the products of that page and return a json with these values.
  • changeFilter(@RequestParam("filter") String filter, @RequestParam("value") String value) It adds the filter sent as parameter, regenerates the info base summary on the new filter; gets the products that comply with the new filter and returns a json with these values.

View

<html xmlns="http://www.w3.org/1999/xhtml"
      class="csstransforms csstransforms3d csstransitions">
<head>
    ...
    <link rel="stylesheet" href="/bower_components/twitterflightexample/twitterflighexample.css"/>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="/bower_components/bootstrap/css/bootstrap.min.css"/>
    <link rel="stylesheet" href="/bower_components/bootstrap/css/bootstrap-responsive.min.css"/>
    <link rel="stylesheet" href="/bower_components/nprogress/nprogress.css"/>
</head>
<body>
    ...
    <script data-main="app2/js/requireMain.js" src="/bower_components/requirejs/require.js"></script>
</body>

Notes:

  • It is a simple html page. Thymeleaf is not needed.
  • bootstrap-paginator: is used to implement the pagination
  • nprogress: is used to show the progress bar at the top during the execution of ajax
  • data-main="app2/js/requireMain.js"; is the main js file. It will be used Require.js (AMD) in order to manage the dependencies and components.

Twitter Flight Components

Define all components:

app.js
define([
    './data/data_provider',
    './ui/product_list',
    './ui/brand_list',
    './ui/breadcrumb',
    './ui/search_box',
    './ui/paginator'
], function (DataProviderData, ProductListUI, BrandListUI, BreadcrumbUI, SearchBoxUI, PaginatorUI) {
    var initialize = function () {
        DataProviderData.attachTo(document);
        ProductListUI.attachTo('#product-zone');
        BrandListUI.attachTo('#brand-zone');
        BreadcrumbUI.attachTo('#breadcrumb-zone');
        SearchBoxUI.attachTo('#search-zone');
        PaginatorUI.attachTo('#paginator-zone');
    };
    return {
        initialize: initialize
    };
});

Notes:

  • The data components normally are attached to document
  • The UI components are attached to the respective html zones.

Data Components

DataProviderData

define(
    ...
function (defineComponent) {
    return defineComponent(dataProvider);
    function dataProvider() {
        ...
        this.onUiPageDataChanged = function (ev, data) {
            console.info("Changing page to:" + data.page);
            var dataString = 'page=' + data.page;
            $.ajax({
                dataType: "json",
                type: "POST",
                url: "/changePage2.htm",
                data: dataString,
                success: function (data) {
                    console.info("ChangPage response:" + data);
                    $(document).trigger("pageDataChanged", {pageInfo: data.pageInfo, productList: data.productList})
                }
            });
        };

        this.onUiFilterChanged = function (ev, data) {
            console.info("Changing filter:" + data.filter + " value:" + data.value);
            var dataString = 'filter=' + data.filter + "&value=" + data.value;
            $.ajax({
                dataType: "json",
                type: "POST",
                url: "/changeFilter2.htm",
                data: dataString,
                success: function (data) {
                    console.info("ChangeFilter response. New number of pages:" + data.totalPages);
                    $(document).trigger("dataChanged", { resultSize: data.totalPages, pageInfo: data.pageInfo, productList: data.productList});
                    $(document).trigger("brandDataChanged", {brandList: data.brandSummaryList});
                    $(document).trigger("breadcrumbChanged", {filterList: data.filterEntryList});
                }
            });
        }

        this.after('initialize', function () {
            this.on('uiPageDataChanged', this.onUiPageDataChanged);
            this.on('uiFilterChanged', this.onUiFilterChanged);
        });
    }
}
Notes:
  • this.after('initialize', function () Registers listeners for the events: uiPageDataChanged,uiFilterChanged
  • this.onUiPageDataChanged = function (ev, data)
    • When the user changes the page, this method will execute an ajax call to: changePage2.htm in order to change the page on the server, this will return a json object.
    • Then it triggers the event pageDataChanged sending the json objects: pageInfo and productList
  • this.onUiFilterChanged = function (ev, data)
    • When the user changes any filter, this method will execute an ajax call to: changeFilter2.htm in order to change the filter on the server, this will return a json object.
    • Then it triggers the events dataChanged,brandDataChanged,breadcrumbChanged sending the json objects: pageInfo, productList,brandList,filterList

UI Components

SearchBoxUI

define(
    ...
function (defineComponent) {
    return defineComponent(searchBox);
    function searchBox() {
        this.defaultAttrs({
            txtSelector: '#txtSearch',
            btnSelector: '#btnSearch'
        });
        this.searchOnClick = function (e) {
            e.preventDefault();
            this.executeSearch();
        };

        this.executeSearch = function () {
            var $txtSearch = this.select('txtSelector');
            var textToSearch = $txtSearch.val().trim();
            if (!textToSearch) {
                return;
            }
            $txtSearch.val('');
            this.trigger("uiFilterChanged", {filter: "name", value: textToSearch})
        }

        this.after('initialize', function () {
            this.on('click', {
                btnSelector: this.searchOnClick
            });
            this.trigger("uiFilterChanged", {filter: "name", value: ""})
        });
    }
}
Notes:
  • this.after('initialize', function ()
    • Registers listeners for the native click event, on the button with id:btnSearch.
    • Trigger the event uiFilterChanged. This is needed in order to populate the initial page with all the products.
  • this.executeSearch = function () Gets the value that is set in the textfield and then triggers the event: uiFilterChanged

Templates

define(
function () {
    var brands =
        '{{#brandList}}\
         <div class="panel-collapse collapse in">\
         ...
        </div>\
        {{/brandList}}';

    var breadcrumb =
        '{{#filterList}}\
        <li>\
            <a href="#" data-filter-idx="{{filterIdx}}">\
            {{value}}\
            </a>\
        </li>\
        {{/filterList}}';
    var breadcrumbActive =
        '<li class="active">\
           <span>\
               {{value}}\
            </span>\
        </li>';

    var productsTitle = '&nbsp;&nbsp;&nbsp;&nbsp;Showing\
        <span>{{currentRangeStr}}</span> de\
        <span>{{productsNbr}}</span>';

    var products =
        '<div class="list-group">\
        {{#productList}}\
            <div class="list-group-item media-hover" data-product-gkey="{{gkey}}">\
            </div>\
            ...
        {{/productList}}\
        </div>';

    return {
        brands: brands,
        breadcrumb: breadcrumb,
        breadcrumbActive: breadcrumbActive,
        productsTitle: productsTitle,
        products: products
    }
}
Notes:
  • brands template used to generate the brand list
  • breadcrumb template used to generate the breadcrumb
  • breadcrumbActive template used to generate the active breadcrumb entry
  • productsTitle template used to generate products title
  • products template used to generate the list of products

BrandListUI

define(
    ...
function (defineComponent, Mustache, templates) {
    return defineComponent(brandList);
    function brandList() {
        this.defaultAttrs({
            divBrand: '#brand-list-zone'
        });

        this.onClick = function (ev, data) {
            ev.preventDefault();
            var $anchor = $(ev.target).closest('.list-group-item').find('a');
            var brandId = $anchor.attr("data-brand-id");
            this.trigger("uiFilterChanged", {filter: "brand", value: brandId})
        };

        this.renderItems = function (ev, data) {
            console.info("On Rendering Brands:" + data);
            var brandHtml = Mustache.render(templates.brands, data);
            var $brand = this.select('divBrand');
            $brand.html(brandHtml);
        };

        this.after('initialize', function () {
            this.on('click', this.onClick);
            this.on(document, 'brandDataChanged', this.renderItems);
        });
    }
}
Notes:
  • this.after('initialize', function () Registers listeners for the native click event, and the custom event brandDataChanged on document
  • this.onClick = function (ev, data) Gets the closest anchor to the html element where the click was done. The brandId is gotten and the event uiFilterChanged is triggered with the brandId as parameter
  • this.renderItems = function (ev, data) Gets the new component's html populating the template templates.brands and then replaces the component's html with this one

BreadcrumbUI

define(
    ...
function (defineComponent, Mustache, templates) {
    return defineComponent(breadcrumb);
    function breadcrumb() {
        ...    
        this.onClick = function (ev, data) {
            ev.preventDefault();
            var filterIdx = $(ev.target).attr("data-filter-idx");
            this.trigger("uiFilterChanged", {filter: "breadcrumbIdx", value: filterIdx})
        }

        this.renderItems = function (ev, data) {
            console.info("On Rendering Breadcrumb");
            var $breadcrumb = this.$node;
            var filterList = data.filterList;
            var last = filterList[filterList.length - 1];
            var breadcrumbActiveHtml = Mustache.render(templates.breadcrumbActive, last);
            var breadcrumbHtml = getBreadcrumbLinks(filterList);
            $breadcrumb.html(breadcrumbHtml + breadcrumbActiveHtml);
        }

        function getBreadcrumbLinks(fltList) {
            var breadcrumbHtml = "";
            if (fltList.length > 1) {
                var list = fltList.slice(0, fltList.length - 1);
                var filterIdx = 0;
                var filterList = {
                    filterList: list,
                    'filterIdx': function () {
                        return function (text, render) {
                            return filterIdx++;
                        }
                    }
                };
                breadcrumbHtml = Mustache.render(templates.breadcrumb, filterList);
            }
            return breadcrumbHtml;
        }

        this.after('initialize', function () {
            this.on('click', this.onClick);
            this.on(document, 'breadcrumbChanged', this.renderItems);
        });
    }
}
);
Notes:
  • this.after('initialize', function () Registers listeners for the native click event, and the custom event breadcrumbChanged on document
  • this.onClick = function (ev, data) Gets the data-filter-idx and triggers the event uiFilterChanged with the breadcrumbIdx as parameter
  • this.renderItems = function (ev, data) Gets the new component's html populating the templates: templates.breadcrumbActive, templates.breadcrumb and then replaces the component's html with this one

ProductListUI

define(
    ...
function (defineComponent, Mustache, templates) {
    return defineComponent(productList);
    function productList() {
        this.defaultAttrs({
            //selectors
            productListZone: "#product-list-zone",
            productListTitle: "#product-list-title"
        });
        this.onClick = function (ev) {
            var $productItem = $(ev.target).closest('.list-group-item');
            var productGkey = $productItem.attr("data-product-gkey");
            showProductInfo($productItem, productGkey);
        }

        function showProductInfo(obj, productGkey) {
            var $this = $(obj);
            var $collapse = $this.find('.collapse');
            var dataString = 'product=' + productGkey;
            var spanId = '#spn' + productGkey;
            var currentText = $(spanId).text();
            if (currentText == "Expand") {
                $(spanId).text("Collapse");
                $.ajax({
                    type: "GET",
                    url: "/getProductInfo.htm",
                    data: dataString,
                    success: function (data) {
                        $collapse.html(data);
                        $collapse.show();
                    }
                });
            } else {
                $(spanId).text("Expand")
                $collapse.hide();
            }
            return false;
        }

        this.renderItems = function (ev, data) {
            console.info("On Rendering Products");
            var $productListTitle = this.select('productListTitle');
            var currentRangePage = getCurrentRangePage(data.pageInfo);
            var pageInfoData = {currentRangeStr: currentRangePage, productsNbr: data.pageInfo.productsNbr}
            var newTitleHtml = Mustache.render(templates.productsTitle, pageInfoData);
            $productListTitle.html(newTitleHtml);
            var $productList = this.select('productListZone');
            var productListHtml = Mustache.render(templates.products, data);
            $productList.html(productListHtml);
        }

        function getCurrentRangePage(pageInfo) {
            var rangeEnd = (pageInfo.currentPage + 1) * pageInfo.pageSize;
            return (pageInfo.currentPage * pageInfo.pageSize + 1) + " - " + Math.min(rangeEnd, pageInfo.productsNbr);
        }

        this.after('initialize', function () {
            this.on(document, 'pageDataChanged', this.renderItems);
            this.on(document, 'dataChanged', this.renderItems);
            this.on('click', this.onClick);
        });
    }
}
Notes:
  • this.after('initialize', function () Registers listeners for the native click event, and the custom event dataChanged and pageDataChanged on document
  • this.onClick = function (ev, data) Gets the closest html element with class: list-group-item from it gets the productGkey and then calls the method showProductInfo
  • showProductInfo(obj, productGkey) Checks if the price history zone is expanded. If so, then it collapses and if not, then it calls the server with /getProductInfo1.htm and then expands and fills the zone of price history with the server response
  • this.renderItems = function (ev, data) Gets the new component's html populating the templates: templates.productsTitle, templates.products and then replaces the component's html with this one

PaginatorUI

define(
    ...
function (defineComponent) {
    return defineComponent(paginator);
    function paginator() {
        this.defaultAttrs({
            options: {
                bootstrapMajorVersion: 3,
                currentPage: 1,
                totalPages: 2,
                numberOfPages: 10
            }
        });

        this.onPageChanged = function (ev, lastPage, currentPage) {
            this.trigger('uiPageDataChanged', {page: (currentPage - 1)});
        };

        this.onDataChanged = function (ev, data) {
            var pagesNumber = data.resultSize;
            var $paginator = $('#paginator');
            if (pagesNumber > 1) {
                $paginator.show();
                this.attr.options.currentPage = 1;
                this.attr.options.totalPages = pagesNumber;
                $paginator.bootstrapPaginator(this.attr.options);
            } else {
                $paginator.hide();
            }
        };

        this.after('initialize', function () {
            this.on('page-changed', this.onPageChanged);
            this.on(document, 'dataChanged', this.onDataChanged);
        });
    }
}
Notes:
This component is a wrapper of bootstrap-paginator
  • this.after('initialize', function () Registers listeners for the bootstrap-paginator's page-changed event, and the custom event dataChanged on document
  • this.onPageChanged = function (ev, lastPage, currentPage) Triggers the uiPageDataChanged event with the page (-1) sent by bootstrap-paginator
  • this.onDataChanged = function (ev, data) Gets the new pages number and based on this the paginator is hidden or shown with the new pages number.

Spring MVC - Thymeleaf - Bootstrap - Twitter Flight - II.1

Source Code | See Application
You can see the full description of the aplication and the other two entries related here

Initial configuration

The web.xml and twitterFlightExample-servlet.xml will be the same as the shown previously.

Controller

We will use the following controller:
@Controller
public class Main1Controller {
    @Autowired
    ProductService productService;
    @Autowired
    MainInfo mainInfo;

    @RequestMapping("/main1")
    public ModelAndView main() {
        return new ModelAndView("main1");
    }

    @RequestMapping(value = "/changePage1", method = RequestMethod.POST)
    @ResponseBody
    public String changePage(@RequestParam("page") Integer page) {
        logger.info("Changing page to:{}", page);
        mainInfo.setCurrentPage(page);
        return "Ok";
    }

    @RequestMapping(value = "/changeFilter1", method = RequestMethod.POST)
    @ResponseBody
    public String changeFilter(@RequestParam("filter") String filter, @RequestParam("value") String value) {
        logger.info("Changing filter to:({}:{})", filter, value);
        mainInfo.updateFilter(filter, value);
        SummaryInfo summaryInfo = productService.createSummaryInfoFor(mainInfo.getFilter());
        mainInfo.setSummaryInfo(summaryInfo);
        return "" + mainInfo.getPageInfo().getTotalPages();
    }

    @RequestMapping("/getProductList1")
    public ModelAndView getProductList() {
        List<Product> products = productService.findProducts(mainInfo.getFilter(), mainInfo.getPageInfo());
        Map model = new HashMap<>();
        model.put("products", products);
        model.put("pageInfo", mainInfo.getPageInfo());
        return new ModelAndView("productList", model);
    }

    @RequestMapping("/getBrandList1")
    public ModelAndView getBrandList() {
        Map model = new HashMap<>();
        model.put("brandList", mainInfo.getSummaryInfo().getBrandSummaryList());
        return new ModelAndView("brandList", model);
    }

    @RequestMapping("/getBreadcrumb1")
    public ModelAndView getBreadcrumb() {
        Map model = new HashMap<>();
        model.put("filterList", mainInfo.getFilter().getFilterEntryList());
        return new ModelAndView("breadcrumb", model);
    }

    @RequestMapping("/getProductInfo1")
    public ModelAndView getProductInfo(@RequestParam("product") Long productGkey) {
        logger.debug("Getting product info for:{}", productGkey);
        List<ProductPriceHistory> productPriceHistoryList = productService.getProductPricesHistoryFor(productGkey);
        Map model = new HashMap();
        model.put("productPriceHistoryList", productPriceHistoryList);
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm");
        model.put("displayDateFormatter", dateFormat);
        return new ModelAndView("productDetail", model);
    }
}

Notes:

  • main() It renders the main page; in this case this page only contains html code so none model needs to be sent.
  • changePage(@RequestParam("page") Integer page) It changes the page to the value sent as parameter and returns Ok
  • changeFilter(@RequestParam("filter") String filter, @RequestParam("value") String value) It adds the filter sent as parameter, regenerates the info base summary on the new filter; and returns the number of total pages.
  • ModelAndView getProductList() It returns the html fragment that contains the product list.
  • ModelAndView getBrandList() It returns the html fragment that contains the brand list.
  • ModelAndView getBreadcrumb() It returns the html fragment that contains the Breadcrumb.
  • getProductInfo(@RequestParam("product") Long productGkey) It returns the html fragment that contains the price information of the product.

View

<html xmlns="http://www.w3.org/1999/xhtml"
      class="csstransforms csstransforms3d csstransitions">
<head>
    ...
    <link rel="stylesheet" href="/bower_components/twitterflightexample/twitterflighexample.css"/>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="/bower_components/bootstrap/css/bootstrap.min.css"/>
    <link rel="stylesheet" href="/bower_components/bootstrap/css/bootstrap-responsive.min.css"/>
    <link rel="stylesheet" href="/bower_components/nprogress/nprogress.css"/>
</head>
<body>
    ...
<script data-main="app1/js/requireMain.js" src="/bower_components/requirejs/require.js"></script>
</body>

Notes:

  • It is a simple html page. Thymeleaf is not needed.
  • bootstrap-paginator: is used to implement the pagination
  • nprogress: is used to show the progress bar at the top during the execution of ajax
  • data-main="app1/js/requireMain.js"; is the main js file. It will be used Require.js (AMD) in order to manage the dependencies and components.

Twitter Flight Components

Define all components:

app.js
define([
    './data/data_provider',
    './ui/product_list',
    './ui/brand_list',
    './ui/breadcrumb',
    './ui/search_box',
    './ui/paginator'
], function (DataProviderData, ProductListUI, BrandListUI, BreadcrumbUI, SearchBoxUI, PaginatorUI) {
    var initialize = function () {
        DataProviderData.attachTo(document);
        ProductListUI.attachTo('#product-zone');
        BrandListUI.attachTo('#brand-zone');
        BreadcrumbUI.attachTo('#breadcrumb-zone');
        SearchBoxUI.attachTo('#search-zone');
        PaginatorUI.attachTo('#paginator-zone');
    };
    return {
        initialize: initialize
    };
});

Notes:

  • The data components normally are attached to document
  • The UI components are attached to the respective html zones.

Data Components

DataProviderData

define(
    ...
    function (defineComponent) {
        return defineComponent(dataProvider);
        function dataProvider() {
            ...
            this.onUiPageDataChanged = function (ev, data) {
                console.info("Changing page to:" + data.page);
                var dataString = 'page=' + data.page;
                $.ajax({
                    type: "POST",
                    url: "/changePage1.htm",
                    data: dataString,
                    success: function (data) {
                        console.info("ChangPage response:" + data);
                        $(document).trigger("pageDataChanged")
                    }
                });
            };

            this.onUiFilterChanged = function (ev, data) {
                console.info("Changing filter:" + data.filter + " value:" + data.value);
                var dataString = 'filter=' + data.filter + "&value=" + data.value;
                $.ajax({
                    type: "POST",
                    url: "/changeFilter1.htm",
                    data: dataString,
                    success: function (data) {
                        console.info("ChangeFilter response. New number of pages:" + data);
                        $(document).trigger("dataChanged", { resultSize: data })
                    }
                });
            }

            this.after('initialize', function () {
                this.on('uiPageDataChanged', this.onUiPageDataChanged);
                this.on('uiFilterChanged', this.onUiFilterChanged);
            });
        }
    }
)
Notes:
  • this.after('initialize', function () Registers listeners for the events: uiPageDataChanged,uiFilterChanged
  • this.onUiPageDataChanged = function (ev, data) When the user changes the page, this method will execute an ajax call to: changePage1.htm in order to change the page on the server.
  • this.onUiFilterChanged = function (ev, data) When the user changes any filter, this method will execute an ajax call to: changeFilter1.htm in order to change the filter on the server.

UI Components

SearchBoxUI

define(
    ...
    function (defineComponent) {
        return defineComponent(searchBox);
        function searchBox() {
            this.defaultAttrs({
                txtSelector: '#txtSearch',
                btnSelector: '#btnSearch'
            });

            this.searchOnClick = function (e) {
                e.preventDefault();
                this.executeSearch();
            };

            this.executeSearch = function () {
                var $txtSearch = this.select('txtSelector');
                var textToSearch = $txtSearch.val().trim();
                if (!textToSearch) {
                    return;
                }
                $txtSearch.val('');
                this.trigger("uiFilterChanged", {filter: "name", value: textToSearch})
            }

            this.after('initialize', function () {
                this.on('click', {
                    btnSelector: this.searchOnClick
                });
                this.trigger("uiFilterChanged", {filter: "name", value: ""})
            });
        }
    }
);
Notes:
  • this.after('initialize', function ()
    • Registers listeners for the native click event, on the button with id:btnSearch.
    • Trigger the event uiFilterChanged. This is needed in order to populate the initial page with all the products.
  • this.executeSearch = function () Gets the value that is set in the textfield and then triggers the event: uiFilterChanged

BrandListUI

define(
...    
function (defineComponent) {
    return defineComponent(brandList);
    function brandList() {
        this.onClick = function (ev, data) {
            ev.preventDefault();
            var $anchor = $(ev.target).closest('.list-group-item').find('a');
            var brandId = $anchor.attr("data-brand-id");
            this.trigger("uiFilterChanged", {filter: "brand", value: brandId})
        };

        this.renderItems = function (ev, data) {
            console.info("On Rendering Brands");
            var $brandList = this.$node;
            $.ajax({
                type: "GET",
                url: "/getBrandList1.htm",
                success: function (data) {
                    $brandList.html(data);
                }
            });
        }

        this.after('initialize', function () {
            this.on('click', this.onClick);
            this.on(document, 'dataChanged', this.renderItems);
        });
    }
}
Notes:
  • this.after('initialize', function () Registers listeners for the native click event, and the custom event dataChanged on document
  • this.onClick = function (ev, data) Gets the closest anchor to the html element where the click was done. The brandId is gotten and the event uiFilterChanged is triggered with the brandId as parameter
  • this.renderItems = function (ev, data) Gets the new component's html from the server, for it executes the call /getBrandList1.htm and then replaces the component's html with the one returned by the server

BreadcrumbUI

define(
    ...
    function (defineComponent) {
    return defineComponent(breadcrumb);
    function breadcrumb() {
        ...
        this.onClick = function (ev, data) {
            ev.preventDefault();
            var filterIdx = $(ev.target).attr("data-filter-idx");
            this.trigger("uiFilterChanged", {filter: "breadcrumbIdx", value: filterIdx})
        }
        this.renderItems = function (ev, data) {
            console.info("On Rendering Breadcrumb");
            var $breadcrumb = this.$node;
            $.ajax({
                type: "GET",
                url: "/getBreadcrumb1.htm",
                success: function (data) {
                    $breadcrumb.html(data);
                }
            });
        }
        this.after('initialize', function () {
            this.on('click', this.onClick);
            this.on(document, 'dataChanged', this.renderItems);
        });
    }
}
Notes:
  • this.after('initialize', function () Registers listeners for the native click event, and the custom event dataChanged on document
  • this.onClick = function (ev, data) Gets the data-filter-idx and triggers the event uiFilterChanged with the breadcrumbIdx as parameter
  • this.renderItems = function (ev, data) Gets the new component's html from the server, for it execute the call /getBreadcrumb1.htm and then replaces the component's html with the one returned by the server

ProductListUI

define(
function (defineComponent) {
    return defineComponent(productList);
    function productList() {
        this.onClick = function (ev) {
            var $productItem = $(ev.target).closest('.list-group-item');
            var productGkey = $productItem.attr("data-product-gkey");
            showProductInfo($productItem, productGkey);
        }

        function showProductInfo(obj, productGkey) {
            var $this = $(obj);
            var $collapse = $this.find('.collapse');
            var dataString = 'product=' + productGkey;
            var spanId = '#spn' + productGkey;
            var currentText = $(spanId).text();
            if (currentText == "Expand") {
                $(spanId).text("Collapse");
                $.ajax({
                    type: "GET",
                    url: "/getProductInfo1.htm",
                    data: dataString,
                    success: function (data) {
                        $collapse.html(data);
                        $collapse.show();
                    }
                });
            } else {
                $(spanId).text("Expand")
                $collapse.hide();
            }
            return false;
        }

        this.renderItems = function (ev, data) {
            console.info("On Rendering Products");
            var $productList = this.$node;
            $.ajax({
                type: "GET",
                url: "/getProductList1.htm",
                success: function (data) {
                    $productList.html(data);
                }
            });
        }

        this.after('initialize', function () {
            this.on(document, 'pageDataChanged', this.renderItems);
            this.on(document, 'dataChanged', this.renderItems);
            this.on('click', this.onClick);
        });
    }
}
Notes:
  • this.after('initialize', function () Registers listeners for the native click event, and the custom event dataChanged and pageDataChanged on document
  • this.onClick = function (ev, data) Gets the closest html element with class: list-group-item from it gets the productGkey and then call the method showProductInfo
  • showProductInfo(obj, productGkey) Checks if the price history zone is expanded. If so, then it collapses and if not, then it calls the server with /getProductInfo1.htm and then expands and fills the zone of price history with the server response
  • this.renderItems = function (ev, data) Gets the new component's html from the server. To do this, it executes the call /getProductList1.htm and then replaces the component's html with the one returned by the server

PaginatorUI

define(
function (defineComponent) {
    return defineComponent(paginator);
    function paginator() {
        this.defaultAttrs({
            options: {
                bootstrapMajorVersion: 3,
                currentPage: 1,
                totalPages: 2,
                numberOfPages: 10
            }
        });

        this.onPageChanged = function (ev, lastPage, currentPage) {
            this.trigger('uiPageDataChanged', {page: (currentPage - 1)});
        };

        this.onDataChanged = function (ev, data) {
            var pagesNumber = data.resultSize;
            var $paginator = $('#paginator');
            if (pagesNumber > 1) {
                $paginator.show();
                this.attr.options.currentPage = 1;
                this.attr.options.totalPages = pagesNumber;
                $paginator.bootstrapPaginator(this.attr.options);
            } else {
                $paginator.hide();
            }
        };

        this.after('initialize', function () {
            this.on('page-changed', this.onPageChanged);
            this.on(document, 'dataChanged', this.onDataChanged);
        });
    }
}
);
Notes:
This component is a wrapper of bootstrap-paginator
  • this.after('initialize', function () Registers listeners for the bootstrap-paginator's page-changed event, and the custom event dataChanged on document
  • this.onPageChanged = function (ev, lastPage, currentPage) Triggers the uiPageDataChanged event with the page (-1) sent by bootstrap-paginator
  • this.onDataChanged = function (ev, data) Gets the new pages number and based on this the paginator is hidden or shown with the new pages number.

Wednesday, June 25, 2014

Spring MVC - Thymeleaf - Bootstrap - I

Source Code | See Application
You can see the full description of the aplication and the other two entries related here

Initial configuration

Web.xml

In this file we define that the Spring DispatcherServlet will be in charge of all the requests that have the ".htm" suffix
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <servlet>
        <servlet-name>twitterFlightExample</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>twitterFlightExample</servlet-name>
        <url-pattern>*.htm</url-pattern>
    </servlet-mapping>
</web-app>

Spring Web Application Context - twitterFlightExample-servlet.xml

For default Spring search the webapplication file with the pattern: [servlet-name]-servlet.xml in this case the file will be: twitterFlightExample-servlet.xml
<beans>
    <import resource="classpath*:META-INF/spring/applicationContext*.xml"/>

    <context:component-scan base-package="org.anotes.example.twitterflight.*"/>

    <bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
        <property name="prefix" value="/pages/"/>
        <property name="suffix" value=".html"/>
        <property name="templateMode" value="HTML5"/>
        <property name="cacheable" value="false"/>
    </bean>

    <bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">
        <property name="templateResolver" ref="templateResolver"/>
    </bean>
    <bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
        <property name="templateEngine" ref="templateEngine"/>
        <property name="order" value="1"/>
    </bean>
</beans>

Notes:

In the templateResolver we define:
  • The path where all of thymeleaf template files:/pages/  will be found 
  • The extension of this templates: .html
  • The template mode: HTML5
  • The cacheable property must be false for developement

Controller

We will use the following controller:
@Controller
public class MainController {
    @Autowired
    ProductService productService;
    @Autowired
    MainInfo mainInfo;

    @RequestMapping("/main")
    public ModelAndView main(HttpServletRequest request) {
        Map requestParams = request.getParameterMap();
        updateMainInfoBasedOn(requestParams);
        ProductFlt productFlt = mainInfo.getFilter();
        SummaryInfo summaryInfo = mainInfo.getSummaryInfo();
        List<Product> products = productService.findProducts(productFlt, mainInfo.getPageInfo());
        Map model = new HashMap<>();
        model.put("products", products);
        model.put("pageInfo", mainInfo.getPageInfo());
        model.put("filterList", productFlt.getFilterEntryList());
        model.put("brandList", summaryInfo.getBrandSummaryList());
        return new ModelAndView("main", model);
    }

    @RequestMapping("/getProductInfo")
    public ModelAndView getProductInfo(@RequestParam("product") Long productGkey) {
        logger.debug("Getting product info for:{}", productGkey);
        List<ProductPriceHistory> productPriceHistoryList = productService.getProductPricesHistoryFor(productGkey);
        Map model = new HashMap();
        model.put("productPriceHistoryList", productPriceHistoryList);
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm");
        model.put("displayDateFormatter", dateFormat);
        return new ModelAndView("productDetail", model);
    }

Notes:

  • ProductService: will be the facade to all of the domain functionality that we need.
  • MainInfo: will be the session object that mantain all the information needed; for instance: the current filter applied, the current page and so on.
  • Methods:
    • main(HttpServletRequest request) It renders the main page; and this will be called also when the filters or the page changes.
    • getProductInfo(@RequestParam("product") Long productGkey) It returns the html fragment that contains the price information of the product. It is called from javascript ajax call, when the user click on the "Expand" link.

View:

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org"
  class="csstransforms csstransforms3d csstransitions">
  <head>
    ....
        <link rel="stylesheet" href="/bower_components/twitterflightexample/twitterflighexample.css"/>
        <!-- Latest compiled and minified CSS -->
        <link rel="stylesheet" href="/bower_components/bootstrap/css/bootstrap.min.css"/>
        <link rel="stylesheet" href="/bower_components/bootstrap/css/bootstrap-responsive.min.css"/>
        <link rel="stylesheet" href="/bower_components/nprogress/nprogress.css"/>
  </head>
<body>
.....
    <div class="panel-collapse collapse in" th:each="brand : ${brandList}">
        <ul class="list-group">
        <li class="list-group-item">
            <a href="#"
               th:href="@{main.htm?(filter=brand,value=${brand.id})}"
               th:text="${brand.description}">Accesorias y Belleza</a>
            <span class="badge" th:text="${brand.nbr}">42</span>
        </li>
        </ul>
    </div>
.....
            <ol id="breadcrumb-zone" class="breadcrumb">
                <li th:each="filterEntry : ${filterList}"
                    th:class="${filterEntryStat.count}==${filterEntryStat.size}?'active'">
                    <a href="#" th:href="@{main.htm?(filter=breadcrumbIdx,value=${filterEntryStat.count}-1)}"
                       th:text="${filterEntry.value}"
                       th:if="${filterEntryStat.count}!=${filterEntryStat.size}">
                        Home</a>
                <span th:text="${filterEntry.value}"
                      th:if="${filterEntryStat.count}==${filterEntryStat.size}">
                    Name2</span>
                </li>
            </ol>
.....
            <a class="pull-left cursorpointer"
           th:onclick="'javascript:showMoreInfo(this,\'' + ${product.gkey} + '\');'">
            <span th:id="spn+${product.gkey}">Expand</span>
           </a>
.....

            <li>Price:
                <strong th:text="${product.price}? ${#numbers.formatDecimal(product.price, 0, 'COMMA', 2, 'POINT')}">123456</strong>
            </li>
.....                
    <script src="/bower_components/jquery/jquery.min.js"></script>
    <script src="/bower_components/bootstrap/js/bootstrap.min.js"></script>
    <script src="/bower_components/bootstrap/js/bootstrap-paginator.min.js"></script>
    <script src="/bower_components/nprogress/nprogress.js"></script>
    <script th:inline="javascript">
/*<![CDATA[*/
var options = {
    bootstrapMajorVersion: 3,
    currentPage: [[${pageInfo.page}]],
    totalPages: [[${pageInfo.totalPages}]],
    numberOfPages: 10,
    onPageClicked: function (e, originalEvent, type, page) {
        window.location.href = "/main.htm?page=" + (page - 1);
    }
};
/*]]>*/
</script>
</body>

Notes:

  • xmlns:th="http://www.thymeleaf.org" This is needed in order to use thymeleaf
  • bootstrap-paginator: is used to implement the pagination
  • nprogress: is used to show the progress bar at the top during the execution of ajax

Thymeleaf

  • Loop: th:each="brand : ${brandList}"
  • Href replace: th:href="@{main.htm?(filter=brand,value=${brand.id})}"
  • Simple text replace: th:text="${brand.description}"
  • Class replace: th:class="${filterEntryStat.count}==${filterEntryStat.size}?'active'"
  • Setting onclick event: th:onclick="'javascript:showMoreInfo(this,\'' + ${product.gkey} + '\');'"
  • Painting zones conditionally th:if="${filterEntryStat.count}!=${filterEntryStat.size}"
  • Strings concatenation th:src="'/img/' +${product.imageUrl}"
  • Format Numbers th:text="${product.price}? ${#numbers.formatDecimal(product.price, 0, 'COMMA', 2, 'POINT')}"
  • Calling methods of objects of the model
    • Adding "displayDateFormatter" to the model:
      public ModelAndView getProductInfo....
          Map model = new HashMap();
          ...
          SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm");
          model.put("displayDateFormatter", dateFormat);
          return new ModelAndView("productDetail", model);
      }
      
    • Calling a method "format" of the object "displayDateFormatter"
      <td th:text="${displayDateFormatter.format(prod.created)}">2.41</td>
  • Inline javascript
    <script th:inline="javascript">
        currentPage: [[${pageInfo.page}]],
        totalPages: [[${pageInfo.totalPages}]],
    </script>
    

Spring MVC - Thymeleaf - Bootstrap - Twitter Flight

Source Code

With the following 3 entries we want to show:

I. How to make a simple web application using Spring MVC, Thymeleaf and Bootstrap; this application will be responsive and will use only one ajax call. See Blog Entry | See Application

II. How to change the previous application in order to be a SPA (single page application). For this part we will use Twitter Flight because it provides us a way to mantain our code nicely separated and easy to make changes to the different components. We will make this possible by using two ways:

  • II.1 The response of the ajax request will return html fragments, and the javascript code only will replace the html content of the different zones.See Blog Entry | See Application
  • II.2 The response of the ajax request will return json objects, and the javascript code will make the following See Blog Entry | See Application:

    • Use the json object to populate the Mustache templates
    • Generate the html fragments using Mustache
    • Replace the html content of the different zones

Application description

The application will be simple and contain the following characteristics:

  • Show products and its price history
  • Show different brands
  • Allow filtering by brands and names
  • Allow pagination

You can see the application here

Notes

  • The application uses gradle so you can execute it with the command: gradle jettyRun
  • Version of the main components
    • Spring = 4.1.0.BUILD-SNAPSHOT
    • Thymeleaf = 2.1.3.RELEASE
    • Jackson = 2.4.1
    • Bootstrap = 3.1.1

Friday, February 21, 2014

Apache Cxf JAX-WS, JAX-RS Example - Translation Service

Source Code

Problem

Make a translator that uses the services of online translation, this translator must then be published using both as JAX-WS as well as JAX-RS

Solution

To solve this problem we will use Web service Code first (Bottom up) approach.

The online translators that will be used are: Google Translator, Microsoft Translator, Systran Translator

The libs that will be used are: Spring Framework 4.0.0, Apache CXF 3.0.0, HttpClient 4.3.2

Structure

We will follow the next structure:

Basic Structure

  1. Core: It contains the main logic that allows us to make the translation, it is formed by:

    1.1. Application Services These are the direct clients of the domain model. They remain very lightweight, coordinating operations performed against domain objects.

    1.2. Domain This is where the business logic and rules of an application live, and it is the heart of the software.

    1.3. Infrastructure This is where general technical, plumbing - related code happens. This layer acts as a supporting library for all the other layers

  2. Interface: This include the classes needed to be able to publish the services using both as JAX-WS as well as JAX-RS

1. Core

1.1 Application Services:

Main interface

public interface TranslatorService {

    TranslatedText translate(String langFrom, String langTo, String text);
}

Main implementation

@Service
public class TranslatorServiceImpl implements TranslatorService {
    @Autowired
    Translator googleTranslator;
    @Autowired
    Translator microsoftTranslator;
    @Autowired
    Translator systranTranslator;

    public TranslatedText translate(String langFrom, String langTo, String text) {
        LanguageSourceTarget languageSourceTarget = new LanguageSourceTarget(Language.fromString(langFrom), Language.fromString(langTo));
        if (languageSourceTarget.sourceAndTargeAreEquals()) {
            throw new TranslatorException("The languages from and to must be differents.");
        }
        Future<String> googleResult = googleTranslator.translate(languageSourceTarget, text);
        Future<String> systranResult = systranTranslator.translate(languageSourceTarget, text);
        Future<String> microsoftResult = microsoftTranslator.translate(languageSourceTarget, text);
        TranslatedText response = new TranslatedText();
        response.setFrom(languageSourceTarget.getSourceAsStr());
        response.setTo(languageSourceTarget.getTargetAsStr());
        response.setMicrosoftTranslation(getTranslation(microsoftResult));
        response.setGoogleTranslation(getTranslation(googleResult));
        response.setSystranTranslation(getTranslation(systranResult));
        return response;
    }

Notes:

  • In this method the 3 translators are called.
  • Future is used in order to execute the translators asynchronously
  • The return class TranslatedText has the 3 translations

1.2. Domain

Main Interface

public interface Translator {

    public Future<String> translate(LanguageSourceTarget languageSourceTarget, String text);
    public String detectLanguage(String text);
}

The pair of languages to be used in translation, is represented by:

public class LanguageSourceTarget {
    private Language source;
    private Language target;

The language to be used in translation, is represented by:

public enum Language {
    AFRIKAANS("af"),
    ALBANIAN("sq"),
    ...

The result of the translation is represented by:

public class TranslatedText {
    private String from;
    private String to;
    private String googleTranslation;
    private String microsoftTranslation;
    private String systranTranslation;

1.3. Infrastructure

Base Implementation of the translators (Template Method)
public abstract class TranslatorImpl implements Translator {
    @Async
    public Future<String> translate(LanguageSourceTarget languageSourceTarget, String text) {
        try {
            String encodedText = URLEncoder.encode(text, ENCODING_UTF_8);
            String from = languageSourceTarget.getSource().asStr();
            String to = languageSourceTarget.getTarget().asStr();
            return new AsyncResult(translateInternal(from, to, text, encodedText));
        } catch (IOException e) {
            LOG.error("Problems translating:" + e.getMessage(), e);
            throw new TranslatorException("Problems translating:" + e.getMessage(), e);
        }
    }

    protected String translateInternal(String from, String to, String text, String encodedText) throws IOException {
        HttpRequestBase requestBase = getHttpRequest(from, to, text, encodedText);
        HttpClient httpclient = HttpClientBuilder.create().build();
        HttpResponse response = httpclient.execute(requestBase);
        HttpEntity responseEntity = response.getEntity();
        String responseAsStr = transformToString(responseEntity);
        if (StringUtils.hasText(responseAsStr)) {
            return getTranslationFrom(responseAsStr);
        }
        return "";
    }

    protected abstract HttpRequestBase getHttpRequest(String from, String to, String text, String encodedText);

    protected abstract String getTranslationFrom(String responseAsStr);

Main methods:

translate:

  • Encode the text using UTF-8
  • Get the string representations of the languages
  • Call to translate internal
  • It is market as Async to mark this method as asynchronous

translateInternal:

  • Get the HttpRequest (GET or POST) to be used.
  • The HttpRequest is invoked using HttpClient, with this we can get the HttpResponse that contains the Entity, which is the response of the translator
  • Convert the Entity to String.
  • Parsing this string we can get the translation

Abstract Methods:

The following methods must be overwritten by each implementation of the translator

HttpRequestBase getHttpRequest(String from, String to, String text, String encodedText)

Return a HttpRequest object that contains: the method (GET or POST), the url that must be called and the parameters that must be sent.

String getTranslationFrom(String responseAsStr)

Receive a String representing the full response of the online translator, and parsing it returns the translation

Google Translator
@Component("googleTranslator")
public class GoogleTranslator extends TranslatorImpl {

    @Override
    protected HttpRequestBase getHttpRequest(String from, String to, String text, String encodedText) {
        HttpGet httpGet = new HttpGet("http://translate.google.com/translate_a/t?client=t&text=" + encodedText + "&hl=" + from + "&sl=" + from + "&tl=" + to + "&multires=1&otf=2&ssel=0&tsel=0&sc=1&ie=" + ENCODING_UTF_8);
        return httpGet;
    }

    protected String getTranslationFrom(String responseAsStr) {
        StringBuilder sb = new StringBuilder();
        if (responseAsStr.length() > 4) {
            int idxEnd = responseAsStr.indexOf("\"", 4);
            sb.append(responseAsStr.substring(4, idxEnd));
        }
        return sb.toString();
    }

Text to translate: "This is a test" (From en to es)

HttpRequestBase GET:

http://translate.google.com/translate_a/t?client=t&text=This+is+a+test&hl=en&sl=en&tl=es&multires=1&otf=2&ssel=0&tsel=0&sc=1&ie=UTF-8

responseAsStr:

[[["Esta es una prueba","This is a test","",""]],,"en",,[["Esta es una prueba",[1],true,false,390,0,4,0]],[["This is a test",1,[["Esta es una prueba",390,true,false],["Esto es una prueba",31,true,false],["esta es una prueba",0,true,false],["Es una prueba",0,true,false],["Este es un examen",0,true,false]],[[0,14]],"This is a test"]],,,[["en"]],103]
Microsoft Translator
@Component("microsoftTranslator")
public class MicrosoftTranslator extends TranslatorImpl {
    private static String API_KEY = "YOUR_API_KEY";

    @Override
    protected HttpRequestBase getHttpRequest(String from, String to, String text, String encodedText) {
        String urlStr = "http://api.microsofttranslator.com/v2/Http.svc/Translate";
        String PARAMETERS = "appId=" + API_KEY + "&text=" + encodedText + "&from=" + from + "&to=" + to + "";
        HttpGet httpget = new HttpGet(urlStr + "?" + PARAMETERS);
        return httpget;
    }

    @Override
    protected String getTranslationFrom(String responseAsStr) {
        return getResultFromResponseStr(responseAsStr);
    }

    public String detectLanguage(String text) {
        try {
            String encodedText = URLEncoder.encode(text, ENCODING_UTF_8);
            String urlStr = "http://api.microsofttranslator.com/v2/Http.svc/Detect";
            String parameters = "appId=" + API_KEY + "&text=" + encodedText;
            HttpGet httpget = new HttpGet(urlStr + "?" + parameters);
            HttpClient httpclient = HttpClientBuilder.create().build();
            HttpResponse response = httpclient.execute(httpget);
            HttpEntity entity = response.getEntity();
            String responseStr = transformToString(entity);
            return getResultFromResponseStr(responseStr);
        } catch (Throwable e) {
            LOG.error("Problems detecting language:" + e.getMessage(), e);
            throw new TranslatorException("Problems detecting language:" + e.getMessage(), e);
        }
    }

    private static String getResultFromResponseStr(String responseAsStr) {
        if (!StringUtils.hasText(responseAsStr)) {
            return "";
        }
        int idxBegin = responseAsStr.indexOf(">");
        int idxEnd = responseAsStr.indexOf("<", idxBegin + 1);
        return responseAsStr.substring(idxBegin + 1, idxEnd);
    }

Text to translate: "This is a test" (From en to es)

HttpRequestBase GET:

http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=YOUR_API_KEY&text=This+is+a+test&from=en&to=es

responseAsStr:

<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">Esto es una prueba</string>
Systran Translator
@Component("systranTranslator")
public class SystranTranslator extends TranslatorImpl {

    @Override
    protected HttpRequestBase getHttpRequest(String from, String to, String text, String encodedText) {
        String lpStr = from + "_" + to;
        String urlStr = "http://www.systranet.com/sai?gui=text&lp=" + lpStr + "&sessionid=13071170317011544&service=urlmarkuptranslate";
        text = "<html><body>" + text + "<br></body></html>";
        StringEntity entityStr = new StringEntity(text, ENCODING_UTF_8);
        HttpPost httpPost = new HttpPost(urlStr);
        httpPost.setEntity(entityStr);
        return httpPost;
    }

    @Override
    protected String getTranslationFrom(String responseAsStr) {
        String classResult = "<html>";
        int idxBegin = responseAsStr.indexOf(classResult);
        idxBegin = responseAsStr.indexOf(classResult, idxBegin + 1);
        int idxEnd = responseAsStr.length() - 1;
        String htmlResult = responseAsStr.substring(idxBegin, idxEnd);
        String result = SimpleHtmlParser.getInnerText(htmlResult);
        return result != null ? result.trim() : "";
    }

Text to translate: "This is a test" (From en to es)

HttpRequestBase POST:

 http://www.systranet.com/sai?gui=text&lp=en_es&sessionid=13071170317011544&service=urlmarkuptranslate

responseAsStr:

<html><body><span class="systran_seg" id="Sp1.s2_o"><span class="systran_token_word" value="2428/pron" id="p1.t2_1">This</span> <span class="systran_token_word" value="4004/verb:plain" id="p1.t2_2">is</span> <span class="systran_token_word" value="3c3e/det" id="p1.t2_3">a</span> <span class="systran_altmeaning" value="&lt\;reference&gt\;test7f7f&lt\;/reference&gt\;&lt\;choice value='altmeaning-,-31322,-,100'&gt\;[criterio normal] (SYSTRAN Alternative Dictionary)&lt\;/choice&gt\;&lt\;choice value='altmeaning-,-31323,-,100'&gt\;[ensayo] (SYSTRAN Alternative Dictionary)&lt\;/choice&gt\;&lt\;choice value='altmeaning-,-31324,-,100'&gt\;[examen] (SYSTRAN Alternative Dictionary)&lt\;/choice&gt\;&lt\;choice value='_main,0' default='yes'&gt\;[prueba] (General)&lt\;/choice&gt\;&lt\;source&gt\;test&lt\;/source&gt\;" id="altmeaning_1"><span class="systran_token_word" value="1010/noun:common" id="p1.t2_4">test</span></span></span><br></body></html>;<html>
<meta http-equiv="Content-Type" content="text/html\; charset=UTF-8">
<body><span class="systran_seg" id="Sp1.s2_o"><span class="systran_token_word" value="2428/pron" id="p1.t2_1">Esto</span> <span class="systran_token_word" value="4004/verb:plain" id="p1.t2_2">es</span> <span class="systran_token_word" value="*" id="p1.t2_0">una</span> <span class="systran_altmeaning" value="&lt\;reference&gt\;test7f7f&lt\;/reference&gt\;&lt\;choice value='altmeaning-,-31322,-,100'&gt\;[criterio normal] (SYSTRAN Alternative Dictionary)&lt\;/choice&gt\;&lt\;choice value='altmeaning-,-31323,-,100'&gt\;[ensayo] (SYSTRAN Alternative Dictionary)&lt\;/choice&gt\;&lt\;choice value='altmeaning-,-31324,-,100'&gt\;[examen] (SYSTRAN Alternative Dictionary)&lt\;/choice&gt\;&lt\;choice value='_main,0' default='yes'&gt\;[prueba] (General)&lt\;/choice&gt\;&lt\;source&gt\;test&lt\;/source&gt\;" id="altmeaning_1"><span class="systran_token_word" value="1010/noun:common" id="p1.t2_4">prueba</span></span></span><br></body></html>;

Testing Translation Service

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/META-INF/spring/applicationContext.xml"})
public class TranslatorServiceTest {

    @Autowired
    TranslatorService translatorService;
    @Test
    public void translateTest() throws Exception {
        TranslatedText translatedText = translatorService.translate("en", "es", "This is a test of translation service");
        System.out.println(translatedText);
    }

It will print:

TranslatedText{from='en', to='es', googleTranslation='Esta es una prueba de servicio de traducción', microsoftTranslation='Esta es una prueba de servicio de traducción', systranTranslation='Ésta es una prueba del servicio de traducción'}

2. Interface

2.1 JAX-RS

Main Interface

@Path("/")
public interface TranslatorRest {

    @GET
    @Path("translate/{from}/{to}/{text}")
    @Produces({MediaType.APPLICATION_JSON})
    public TranslatedText translate(@PathParam("from") String from, @PathParam("to") String to, @PathParam("text") String text);
}

Main Implementation

@Service
public class TranslatorRestImpl implements TranslatorRest {

    @Autowired
    TranslatorService translatorService;

    public TranslatedText translate(String from, String to, String text) {
        TranslatedText translatedText = translatorService.translate(from, to, text);
        return translatedText;
    }
}

Notes:

  • It is only required to put the JAX-RS annotations in the interface
  • The main method only calls the application service. There is no business logic in these methods
Exception Handler
@Component
public class ExceptionHandler implements ExceptionMapper {

    public javax.ws.rs.core.Response toResponse(Throwable throwable) {
        return Response.serverError().entity(throwable.getMessage()).build();
    }
}
Examples:

Request:

http://services.anotes.org/translator/translate/en/es/task

Response:

{"from":"en","to":"es","googleTranslation":"tarea","microsoftTranslation":"tarea","systranTranslation":"tarea"}
Using filters

We need that this service can support links as the following:

http://services.anotes.org/translator/translate/task

In this there is not any information about the language neither "from" nor "to" but we can infer both of them and convert this link to:

http://services.anotes.org/translator/translate/en/es/task

To infer the "from" language we can use the detectLanguage method of MicrosoftTranslator using the text to translate as parameter To infer the "to" language we can use the language sent in the request header.

To convert the url before calling the TranslatorRest we must use a filter. For this case we will implement ContainerRequestFilter as show below

Container Request Filter

@Component
@Provider
@PreMatching
public class LanguageSetterFilter implements ContainerRequestFilter {
    private static final String DEFAULT_LANG_TO = "es";
    private static final String PATH_TRANSLATE = "/translate/";
    @Autowired
    Translator microsoftTranslator;

    public void filter(ContainerRequestContext ctx) {
        UriInfo uriInfo = ctx.getUriInfo();
        String uriStr = uriInfo.getRequestUri().toString();
        int idx = uriStr.indexOf(PATH_TRANSLATE);
        boolean isTranslatePath = idx != -1;
        if (isTranslatePath) {
            String parameters = uriStr.substring(idx + PATH_TRANSLATE.length());
            boolean existLanguages = parameters.indexOf("/") != -1;
            if (!existLanguages) {
                String headerLanguage = getHeaderLanguage(ctx);
                ctx.setRequestUri(getNewUri(uriStr, idx, parameters, headerLanguage));
            }
        }
    }

    private String getHeaderLanguage(ContainerRequestContext ctx) {
        String headerString = ctx.getHeaderString("accept-language");
        return headerString != null && headerString.length() > 2 ? headerString.substring(0, 2) : DEFAULT_LANG_TO;
    }

    private URI getNewUri(String uriStr, int idx, String parameters, String headerLanguage) {
        String langFrom = microsoftTranslator.detectLanguage(parameters);
        String langTo = headerLanguage;
        String newUri = uriStr.substring(0, idx + PATH_TRANSLATE.length()) + langFrom + "/" + langTo + "/" + parameters;
        try {
            return new URI(newUri);
        } catch (URISyntaxException e) {
            LOG.error("Getting new uri:" + newUri, e);
            throw new TranslatorException("Problems Getting new uri:" + newUri, e);
        }
    }
}

Method

filter:

JAX-RS Bean Declaration
<jaxrs:server address="/translator">
    <jaxrs:serviceBeans>
        <ref bean="translatorRestImpl"/>
    </jaxrs:serviceBeans>
    <jaxrs:providers>
        <ref bean="languageSetterFilter"/>
        <ref bean="exceptionHandler"/>
        <bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider"/>
    </jaxrs:providers>
</jaxrs:server>
Wadl
<application>
    <grammars/>
    <resources base="http://services.gamal-mateo.cloudbees.net/translator">
        <resource path="/">
            <resource path="translate/{from}/{to}/{text}">
                <param name="from" style="template" type="xs:string"/>
                <param name="to" style="template" type="xs:string"/>
                <param name="text" style="template" type="xs:string"/>
                <method name="GET">
                    <request/>
                    <response>
                        <representation mediaType="application/json"/>
                    </response>
                </method>
            </resource>
        </resource>
    </resources>
</application>
Testing
public class TranslatorRestTest {
    private final static String ENDPOINT_ADDRESS = "http://localhost:8062/apache-cxf-example/translator";
    public static final String ENCODING_UTF_8 = "UTF-8";

    @Test
    public void translateTest() throws IOException {
        Client client = ClientBuilder.newClient();
        Response response = client.target(ENDPOINT_ADDRESS + "/translate/es/en/prueba").request("application/json").get();
        String responseStr = getResponseAsStr(response);
        System.out.println(responseStr);
    }

It will print:

{"from":"es","to":"en","googleTranslation":"test","microsoftTranslation":"","systranTranslation":"test"}

2.1 JAX-WS

Main Interface

@WebService
public interface TranslatorPT {

    @SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
    @WebMethod(action = "http://anotes.org/services/translator/ws/translate")
    @WebResult(name = "TranslateResponse", targetNamespace = "http://anotes.org/services/translator/ws/schema", partName = "part")
    @ResponseWrapper(targetNamespace = "http://anotes.org/services/translator/ws/schema", className = "org.anotes.services.translator.ws.schema.TranslateResponse")
    @RequestWrapper(targetNamespace = "http://anotes.org/services/translator/ws/schema", className = "org.anotes.services.translator.ws.schema.TranslateRequest")
    public TranslateResponse translate(@WebParam(name = "translateRequest",
            partName = "part",
            targetNamespace = "http://anotes.org/services/translator/ws/schema")
                                       TranslateRequest request);

}

Main Implementation

@Service
@WebService(portName = "translatorPort",
        serviceName = "translatorService",
        targetNamespace = "http://anotes.org/services/translator/ws",
        endpointInterface = "org.anotes.services.translator.ws.TranslatorPT")
public class TranslatorPTImpl implements TranslatorPT {

    @Autowired
    TranslatorService translatorService;

    @Autowired
    ConversionService conversionService;

    public TranslateResponse translate(TranslateRequest request) {
        TranslatedText translatedText = translatorService.translate(request.getLangFrom(), request.getLangTo(), request.getText());
        TranslateResponse response = conversionService.convert(translatedText, TranslateResponse.class);
        return response;
    }
}

Notes:

  • It is only required to put the full JAX-WS annotations in the interface methods
  • It is required to put the full class JAX-WS @WebService annotations in the implementation class
  • The main method only calls the application service. There is no business logic in these methods

Request

public class TranslateRequest {
    private String langFrom;
    private String langTo;
    private String text;

Response

public class TranslateResponse {
    private ResultEnum resultCode = ResultEnum.OK;
    private String errorMsg;
    private String googleTranslation;
    private String microsoftTranslation;
    private String systranTranslation;

package-info It is needed in order to point where will be generated the objects for this web service

@javax.xml.bind.annotation.XmlSchema(namespace = "http://anotes.org/services/translator/ws/schema")
package org.anotes.services.translator.ws.schema;
Exception Handler

We will use spring-aop; specifically we will use the following aspect in order to intercept all calls to public methods of the *PTImpl class; and in case of exception we will generate an object of TranslateResponse setting the result code and the error message

@Aspect
@Component
public class WsServicesAspect {
    protected final Logger LOG = LoggerFactory.getLogger(getClass());

    @Pointcut("bean(*PTImpl)")
    private void serviceBean() {
    }

    @Pointcut("execution(public * *(..))")
    private void publicMethod() {
    }

    @Around("serviceBean() && publicMethod()")
    public Object processServicePtPublicMethods(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        String method = proceedingJoinPoint.getSignature().toShortString();
        try {
            LOG.info("Around: Before executing:" + method);
            Object obj = proceedingJoinPoint.proceed();
            LOG.info("Around: After executing:" + method);
            return obj;
        } catch (Throwable throwable) {
            LOG.error("Problems calling the method: " + method, throwable);
            String errorMsg = throwable.getMessage();
            Object responseObj = getResponseInstance(proceedingJoinPoint);
            BeanWrapper beanWrapper = new BeanWrapperImpl(responseObj);
            beanWrapper.setPropertyValue("resultCode", ResultEnum.ERROR);
            beanWrapper.setPropertyValue("errorMsg", errorMsg);
            return responseObj;
        }
    }
JAX-WS Bean Declaration
<!-- JAX-WS -->
<jaxws:endpoint address="/translatorPort"
                implementor="#translatorPTImpl"/>
Wsdl
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                  xmlns:tns="http://anotes.org/services/translator/ws"
                  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                  xmlns:ns1="http://ws.translator.services.anotes.org/" name="translatorService"
                  targetNamespace="http://anotes.org/services/translator/ws">
    <wsdl:import location="http://services.gamal-mateo.cloudbees.net/translatorPort?wsdl=TranslatorPT.wsdl"
                 namespace="http://ws.translator.services.anotes.org/"></wsdl:import>
    <wsdl:binding name="translatorServiceSoapBinding" type="ns1:TranslatorPT">
        <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
        <wsdl:operation name="translate">
            <soap:operation soapAction="http://anotes.org/services/translator/ws/translate" style="document"/>
            <wsdl:input name="translate">
                <soap:body use="literal"/>
            </wsdl:input>
            <wsdl:output name="translateResponse">
                <soap:body use="literal"/>
            </wsdl:output>
        </wsdl:operation>
    </wsdl:binding>
    <wsdl:service name="translatorService">
        <wsdl:port binding="tns:translatorServiceSoapBinding" name="translatorPort">
            <soap:address location="http://services.gamal-mateo.cloudbees.net/translatorPort"/>
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>
Testing
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/META-INF/spring/spring-context-test.xml"})
public class TranslatorPTTest {

    @Autowired
    private TranslatorPT wsClient;

    @Test
    public void translateTest() {
        TranslateRequest request = new TranslateRequest();
        request.setLangFrom("es");
        request.setLangTo("en");
        request.setText("Esta es una prueba de JAXWS");
        TranslateResponse response = wsClient.translate(request);
        System.out.println(response);
    }

It will print:

TranslateResponse{resultCode=OK, errorMsg='null', googleTranslation='This is a test of JAXWS', microsoftTranslation='', systranTranslation='This is a test of JAXWS'}

Final Notes:

  • Microsoft translator needs an API KEY, so you have to request one and then put it on the private static String API_KEY = "YOUR_API_KEY" in MicrosoftTranslator

  • Gradle is used so you can run the application executing: gradle jettyrun