Thứ Bảy, 24 tháng 3, 2012

JSF - Bài 9: Doanh thu toàn công ty


Lập trình Web JavaServer Faces


Center of Excellence


Bài 9
Doanh thu toàn công ty



I. Bài toán


Hãy phát triển một web application để tính tổng doanh thu toàn năm của một công ty thương mại. Sau đây là ví dụ về doanh thu 4 quí trong năm của 3 chi nhánh thuộc công ty




Quí 1
Quí 2
Quí 3
Quí 4
Chi nhánh 1
$35,698.77
$36,148.63
$31,258.95
$30,864.12
Chi nhánh 2
$41,289.64
$43,278.52
$40,928.18
$42,818.98
Chi nhánh 3
$28914.56
$27631.52
$30596.64
$29834.21



II. Giải pháp


Project: TotalSalesJSF

1. Thiết kế UI


Trước tiên application cần biết công ty có bao nhiêu chi nhánh (divisions), để từ đó xuất ra số lượng ô nhập thích hợp.

Hình 1

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    <h:head>
        <title>Yearly Sales</title>
    </h:head>
    <h:body>
        <h2>Yearly Sales</h2>
        <h:form prependId="false">
            <label>Number of divisions:</label>
            <h:inputText id="divisions" required="true" value="#{index.divisions}">
                <f:validateLongRange minimum="1"/>
            </h:inputText>
            <h:message for="divisions" errorStyle="color:red"/><br/>
            
            <div>
                <h:commandButton value="Input Sales"/>
                <h:commandButton value="Clear"/>
            </div>
        </h:form>
        <h:messages errorStyle="color:red"/>
    </h:body>
</html>

Hình 2

package controller;

import javax.enterprise.context.RequestScoped;
import javax.inject.Named;

@Named(value = "index")
@RequestScoped
public class Index {
    private Integer divisions;

    public Index() {
    }

    public Integer getDivisions() {
        return divisions;
    }

    public void setDivisions(Integer divisions) {
        this.divisions = divisions;
    }
}

Một khi đã biết được số chi nhánh (divisions), application cần khởi tạo tổng cộng (divisions x 4) ô nhập, do mỗi chi nhánh có các doanh số (sales figures) cho 4 quí. Số ô nhập này có thể được tổ chức trong một two-dimensional array.

Hình 3

@Named(value = "index")
@RequestScoped
public class Index {
    private Integer divisions;
    private SalesFigure[][] salesFigures;

    public Index() {
    }
    
    public void initSalesFigures() {
        final int QUARTERS = 4;
        salesFigures = new SalesFigure[divisions][QUARTERS];
        for (int i = 0; i < divisions; i++)
            for (int j = 0; j < QUARTERS; j++)
                salesFigures[i][j] = new SalesFigure();
    }

    public Integer getDivisions() {
        return divisions;
    }

    public void setDivisions(Integer divisions) {
        this.divisions = divisions;
    }

    public SalesFigure[][] getSalesFigures() {
        return salesFigures;
    }
}

Về mặt UI, khi user clicks Input Sales button, initSalesFigures() cần được thực hiện, ta bổ sung action attribute cho button này.

<h:form prependId="false">
    <label>Number of divisions:</label>
    <h:inputText id="divisions" required="true" value="#{index.divisions}">
        <f:validateLongRange minimum="1"/>
    </h:inputText>
    <h:message for="divisions" errorStyle="color:red"/><br/>
    
    <div>
        <h:commandButton value="Input Sales" action="#{index.initSalesFigures()}"/>
        <h:commandButton value="Clear"/>
    </div>
</h:form>
Sau đó, application cần hiển thị các ô nhập để user có thể nhập doanh thu vào đó.

Hình 4

<h:form prependId="false">
    <label>Number of divisions:</label>
    <h:inputText id="divisions" required="true" value="#{index.divisions}">
        <f:validateLongRange minimum="1"/>
    </h:inputText>
    <h:message for="divisions" errorStyle="color:red"/><br/>
    
    <div>
        <h:commandButton value="Input Sales" action="#{index.initSalesFigures()}"/>
        <h:commandButton value="Clear"/>
    </div>
    
    <table>
        <thead>
            <tr>
                <th></th>
                <th>Quarter 1</th>
                <th>Quarter 2</th>
                <th>Quarter 3</th>
                <th>Quarter 4</th>
            </tr>
        </thead>
        <tbody>
            <ui:repeat id="Row" var="division" value="#{index.salesFigures}">
                <tr>
                    <td>Division ???</td>
                    <ui:repeat id="Column" var="salesFigure" value="#{division}">
                        <td>
                            <h:inputText id="SalesFigure" value="#{salesFigure.value}" required="true">
                                <f:validateDoubleRange minimum="0.0"/>
                            </h:inputText>
                        </td>
                    </ui:repeat>
                </tr>
            </ui:repeat>
        </tbody>
    </table>
</h:form>

Để hiển thị đúng số thứ tự của các divisions, ta lợi dụng varStatus attribute của <ui:repeat>, trong đó index bắt đầu từ 0.

<tbody>
    <ui:repeat id="Row" var="division" value="#{index.salesFigures}" varStatus="status">
        <tr>
            <td>Division #{status.index + 1}</td>
            <ui:repeat id="Column" var="figure" value="#{division}">
                <td>
                    <h:inputText id="SalesFigure" value="#{salesFigure.value}" required="true">
                        <f:validateDoubleRange minimum="0.0"/>
                    </h:inputText>
                </td>
            </ui:repeat>
        </tr>
    </ui:repeat>
</tbody>

Điều cần chú ý nữa là các ô nhập này chỉ xuất hiện sau khi user đã nhập vào số divisions. Ta xử lý vấn đề này bằng cách đặt <table> vào một <h:panelGroup> và qui định rằng <h:panelGroup> này chỉ được hiển thị (rendered) nếu salesFigures đã được khởi tạo, hay nói cách khác, salesFigures là không rỗng (not empty).

<h:panelGroup rendered="#{not empty index.salesFigures}">
    <table>
        <thead>
            <tr>
                <th></th>
                <th>Quarter 1</th>
                <th>Quarter 2</th>
                <th>Quarter 3</th>
                <th>Quarter 4</th>
            </tr>
        </thead>
        <tbody>
            <ui:repeat id="Row" var="division" value="#{index.salesFigures}" varStatus="status">
                <tr>
                    <td>Division #{status.index + 1}</td>
                    <ui:repeat id="Column" var="salesFigure" value="#{division}">
                        <td>
                            <h:inputText id="SalesFigure" value="#{salesFigure.value}" required="true">
                                <f:validateDoubleRange minimum="0.0"/>
                            </h:inputText>
                        </td>
                    </ui:repeat>
                </tr>
            </ui:repeat>
        </tbody>
    </table>
</h:panelGroup>

Sau khi user đã nhập xong doanh thu, ta cần thêm một button để tính tổng và một dòng thông báo kết quả tính toán.

<h:panelGroup rendered="#{not empty index.salesFigures}">
    <table>
        <thead>
            <tr>
                <th></th>
                <th>Quarter 1</th>
                <th>Quarter 2</th>
                <th>Quarter 3</th>
                <th>Quarter 4</th>
            </tr>
        </thead>
        <tbody>
            <ui:repeat id="Row" var="division" value="#{index.salesFigures}" varStatus="status">
                <tr>
                    <td>Division #{status.index + 1}</td>
                    <ui:repeat id="Column" var="salesFigure" value="#{division}">
                        <td>
                            <h:inputText id="SalesFigure" value="#{salesFigure.value}" required="true">
                                <f:validateDoubleRange minimum="0.0"/>
                            </h:inputText>
                        </td>
                    </ui:repeat>
                </tr>
            </ui:repeat>
        </tbody>
    </table>
            
    <h:commandButton value="Compute Total Sales" action="#{index.computeTotalSales()}"/><br/>

    <h:panelGroup rendered="#{not empty index.totalSales}">
        <label>Total Sales:</label>
        <h:inputText value="#{index.totalSales}" disabled="true"/>
    </h:panelGroup>
</h:panelGroup>
@Named(value = "index")
@RequestScoped
public class Index {
    private Integer divisions;
    private SalesFigure[][] salesFigures;
    private Double totalSales;

    public Index() {
    }
    
    public void initSalesFigures() {
        final int QUARTERS = 4;
        salesFigures = new SalesFigure[divisions][QUARTERS];
        for (int i = 0; i < divisions; i++)
            for (int j = 0; j < QUARTERS; j++)
                salesFigures[i][j] = new SalesFigure();
    }
    
    public void computeTotalSales() {
        // TODO
    }

    public Double getTotalSales() {
        return totalSales;
    }
    ...
}
Đến đây nếu thử thi hành application, nhập vào số divisions, rồi click Input Sales button, ta sẽ được kết quả sau

Hình 5

Bây giờ nếu click Compute Total Sales thì các ô nhập sẽ biến mất (!). Lý do là vì ta đã khai báo @RequestScoped cho managed bean. Bản chất của request scope là không nhớ lại data của lần tương tác trước để dùng lại cho lần tương tác sau. Khi muốn dùng lại data của lần tương tác trước, ta cần đặt managed bean vào session scope.

@Named(value = "index")
@SessionScoped
public class Index implements Serializable {
    private Integer divisions;
    private SalesFigure[][] salesFigures;
    private Double totalSales;
    ...
}


2. Phát triển model


Đến đây ta tập trung phát triển computeTotalSales(). Như đã đề cập trong bài Doanh thu trong tuần, ta dùng SalesFigure wrapper class chỉ nhằm tương tác với UI. Khi thực hiện việc tính toán, ta tháo bỏ lớp vỏ bọc này.

@Named(value = "index")
@SessionScoped
public class Index implements Serializable {
    private Integer divisions;
    private SalesFigure[][] salesFigures;
    private Double totalSales;

    public Index() {
    }
    
    public void initSalesFigures() {
        final int QUARTERS = 4;
        salesFigures = new SalesFigure[divisions][QUARTERS];
        for (int i = 0; i < divisions; i++)
            for (int j = 0; j < QUARTERS; j++)
                salesFigures[i][j] = new SalesFigure();
    }
    
    public void computeTotalSales() {
        double[][] figures = salesFiguresToDoubles();
        totalSales = ???;
    }
    
    private double[][] salesFiguresToDoubles() {
        int rows = salesFigures.length;
        int columns = salesFigures[0].length;
        double[][] figures = new double[rows][columns];
        for (int i = 0; i < rows; i++)
            for (int j = 0; j < columns; j++)
                figures[i][j] = salesFigures[i][j].getValue();
        return figures;
    }

    ...
}

2.1. Design


Hình 6


2.2. Test class


package model;

import org.junit.Test;
import static org.junit.Assert.*;

public class YearlySalesTest {
    @Test public void sum() {
        double[][] quarterlySales = {
            { 35_698.77, 36_148.63, 31_258.95, 30_864.12 },
            { 41_289.64, 43_278.52, 40_928.18, 42_818.98 },
            { 28_914.56, 27_631.52, 30_596.64, 29_834.21 } };
        assertEquals(419_262.72, new YearlySales(quarterlySales).sum(), 6E-11);
    }
}


2.3. Main class


YearlySales.java

package model;

import model.util.ArrayUtils;

public class YearlySales {
    private final double[][] quarterlySales;

    public YearlySales(double[][] quarterlySales) {
        this.quarterlySales = quarterlySales;
    }
    
    public double sum() {
        double acc = 0.0;
        for (double[] divisionalSales : quarterlySales)
            acc += ArrayUtils.sum(divisionalSales);
        return acc;
    }
}

YearlySales đã lợi dụng class bổ trợ là ArrayUtils để thực hiện việc tính tổng.

ArrayUtils.java

package model.util;

public class ArrayUtils {
    private ArrayUtils() {
        throw new RuntimeException(this.getClass() +
                " is a noninstantiable utility class");
    }
    
    public static double sum(double[] values) {
        double acc = 0.0;
        for (double each : values)
            acc += each;
        return acc;
    }
    
    public static double sum(double[][] values) {
        double acc = 0.0;
        for (double[] row : values)
            acc += sum(row);
        return acc;
    }
    
    public static double average(double[] values) {
        return sum(values) / values.length;
    }
    
    public static double min(double[] values) {
        double currentMin = Double.POSITIVE_INFINITY;
        for (double value : values) {
            if (value < currentMin)
                currentMin = value;
        }
        return currentMin;
    }
    
    public static double max(double[] values) {
        double currentMax = Double.NEGATIVE_INFINITY;
        for (double value : values) {
            if (value > currentMax)
                currentMax = value;
        }
        return currentMax;
    }
}


3. Kết nối managed bean với model


@Named(value = "index")
@SessionScoped
public class Index implements Serializable {
    private Integer divisions;
    private SalesFigure[][] salesFigures;
    private Double totalSales;

    ...
    
    public void computeTotalSales() {
        double[][] figures = salesFiguresToDoubles();
        totalSales = new YearlySales(figures).sum();
    }
    
    ...
}


4. Định dạng số


Khi thi hành application, ta nhận được kết quả sau

Hình 7

Để tránh sai sót trong quá trình nhập liệu, ta cần cho phép user sử dụng dấu cách hàng ngàn. Chẳng hạn, thay vì phải nhập 35698.77, user có thể nhập 35,698.77. Thêm vào đó, ta cần định dạng kết quả sao cho chỉ hiển thị hai chữ số sau dấu chấm thập phân. Cách thức định dạng số đã được đề cập trong bài Doanh thu trong tuần. Ta có code bổ sung cho phần UI như sau

<h:panelGroup rendered="#{not empty index.salesFigures}">
    <table>
        <thead>
            <tr>
                <th></th>
                <th>Quarter 1</th>
                <th>Quarter 2</th>
                <th>Quarter 3</th>
                <th>Quarter 4</th>
            </tr>
        </thead>
        <tbody>
            <ui:repeat id="Row" var="division" value="#{index.salesFigures}" varStatus="status">
                <tr>
                    <td>Division #{status.index + 1}</td>
                    <ui:repeat id="Column" var="salesFigure" value="#{division}">
                        <td>
                            <h:inputText id="SalesFigure" value="#{salesFigure.value}" required="true">
                                <f:convertNumber pattern="#,##0.00"/>
                                <f:validateDoubleRange minimum="0.0"/>
                            </h:inputText>
                        </td>
                    </ui:repeat>
                </tr>
            </ui:repeat>
        </tbody>
    </table>
            
    <h:commandButton value="Compute Total Sales" action="#{index.computeTotalSales()}"/><br/>

    <h:panelGroup rendered="#{not empty index.totalSales}">
        <label>Total Sales:</label>
        <h:inputText value="#{index.totalSales}" disabled="true">
            <f:convertNumber pattern="#,##0.00"/>
        </h:inputText>
    </h:panelGroup>
</h:panelGroup>

Khi đó, UI screen sẽ trông như sau

Hình 8


5. Phát triển clear()


index.xhtml

<h:head>
    <title>Yearly Sales</title>
    <script src="resources/clearInputTexts.js"/>
</h:head>
<h:body>
    <h2>Yearly Sales</h2>
    <h:form prependId="false">
        <label>Number of divisions:</label>
        <h:inputText id="divisions" required="true" value="#{index.divisions}">
            <f:validateLongRange minimum="1"/>
        </h:inputText>
        <h:message for="divisions" errorStyle="color:red"/><br/>
            
        <div>
            <h:commandButton value="Input Sales" action="#{index.initSalesFigures}"/>
            <h:commandButton value="Clear" action="#{index.clear()}" immediate="true"
                             onclick="clearInputTexts(this.form)"/>
        </div>
        ...
</h:body>

Index.java

@Named(value = "index")
@SessionScoped
public class Index implements Serializable {
    private Integer divisions;
    private SalesFigure[][] salesFigures;
    private Double totalSales;

    public Index() {
    }
    
    public void clear() {
        divisions = null;
        salesFigures = null;
        totalSales = null;
    }
    ...
}


III. Tổng kết


Tiếp nối bài Doanh thu trong tuần, bài này tiếp tục dùng session scope để duy trì dữ liệu nhập, phục vụ cho việc tính tổng doanh thu toàn năm của một công ty. Thêm vào đó, bài đã trình bày kỹ thuật hiển thị và che dấu một phần UI thông qua việc sử dụng <h:panelGroup> tag cùng với rendered attribute của nó.


IV. Tài liệu tham khảo

  1. Tony Gaddis (2010) Starting Out with Java From Control Structures Through Objects (4th edition), Pearson Education, Boston. Chương 8.