/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package openbook.client; import java.awt.BorderLayout; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.GroupLayout; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SpinnerNumberModel; import javax.swing.SwingConstants; import javax.swing.SwingWorker; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import jpa.tools.swing.EntityDataModel; import jpa.tools.swing.EntityTable; import jpa.tools.swing.EntityTableView; import jpa.tools.swing.ErrorDialog; import openbook.client.Demo.ShowCodeAction; import openbook.domain.Author; import openbook.domain.Book; import openbook.domain.Customer; import openbook.domain.PurchaseOrder; import openbook.domain.ShoppingCart; import openbook.server.OpenBookService; import openbook.server.QueryDecorator; import org.apache.openjpa.lib.jdbc.SQLFormatter; /** * A visual page coordinates the following functions of {@link OpenBookService} : * <li>query for books * <li>choose one or more of the selected books * <li>add them to Shopping Cart * <li>place a purchase order of the books in the shopping cart. * <p> * Each interaction with the underlying service occurs in a background i.e. * a <em>non</em>-AWT event dispatching thread. The background threads are * used via {@link SwingWorker} and hence each persistence operation is * can be potentially handled by a different JPA persistence context. * This threading model not only adheres to the good practice of responsive graphical * user interface design, it exercises the <em>remote</em> nature of JPA service * (even within this single process Swing application) where every operation * on a persistence context results into a set of <em>detached</em> instances * to the remote client. * * @author Pinaki Poddar */ @SuppressWarnings("serial") public final class BuyBookPage extends JPanel { private final OpenBookService _service; private final Customer _customer; private final SearchPanel _searchPanel; private final BuyPanel _buyPanel; private final SelectBookPanel _selectPanel; private final ShoppingCartPanel _cartPanel; /** * A Page with 2x2 Grid of panels each for one of the specific action. * * @param service the OpenBooks service handle. */ public BuyBookPage(OpenBookService service, Customer customer) { super(true); _service = service; _customer = customer; GridLayout gridLayout = new GridLayout(2, 2); this.setLayout(gridLayout); _searchPanel = new SearchPanel("Step 1: Search for Books"); _selectPanel = new SelectBookPanel("Step 2: Select Books to view details"); _buyPanel = new BuyPanel("Step 3: Add Book to Shopping Cart"); _cartPanel = new ShoppingCartPanel("Step 4: Purchase Books in the Shopping Cart"); add(_searchPanel); add(_cartPanel); add(_selectPanel); add(_buyPanel); _selectPanel._selectedBooks.getTable() .getSelectionModel() .addListSelectionListener(_buyPanel); } /** * A form like panel displays the different fields to search for books. * Zero or more form fields can be filled in. Though the service level * contract does not mandate how to form the exact query from the form * field values, the actual implementation demonstrates how dynamic * query construction introduced via Criteria Query API in JPA 2.0 * can aid in such common user scenarios. * <br> * The object level query is displayed to demonstrate the ability of * OpenJPA to express a dynamic Criteria Query is a human-readable, * JPQL-like query string. * * @author Pinaki Poddar * */ class SearchPanel extends JPanel implements ActionListener { private final JTextField _title = new JTextField("", 20); private final JTextField _author = new JTextField("", 20); private final JTextField _maxPrice = new JTextField("", 6); private final JTextField _minPrice = new JTextField("", 6); private final JTextArea _queryView = new JTextArea(); private final SQLFormatter _formatter = new SQLFormatter(); SearchPanel(String title) { super(true); setBorder(BorderFactory.createTitledBorder(title)); JLabel titleLabel = new JLabel("Title :", SwingConstants.RIGHT); JLabel authorLabel = new JLabel("Author:", SwingConstants.RIGHT); JLabel priceLabel = new JLabel("Price :", SwingConstants.RIGHT); JLabel fromLabel = new JLabel("from ", SwingConstants.RIGHT); JLabel toLabel = new JLabel("to ", SwingConstants.RIGHT); JPanel panel = new JPanel(); GroupLayout layout = new GroupLayout(panel); panel.setLayout(layout); layout.setAutoCreateContainerGaps(true); layout.setAutoCreateGaps(true); GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); hGroup.addGroup(layout.createParallelGroup() .addComponent(titleLabel) .addComponent(authorLabel) .addComponent(priceLabel)); hGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.CENTER) .addComponent(_title) .addComponent(_author) .addGroup(layout.createSequentialGroup() .addComponent(fromLabel) .addComponent(_minPrice) .addComponent(toLabel) .addComponent(_maxPrice))); GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); vGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.CENTER) .addComponent(titleLabel) .addComponent(_title)); vGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.CENTER) .addComponent(authorLabel) .addComponent(_author)); vGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.CENTER) .addComponent(priceLabel) .addComponent(fromLabel) .addComponent(_minPrice) .addComponent(toLabel) .addComponent(_maxPrice)); layout.setHorizontalGroup(hGroup); layout.setVerticalGroup(vGroup); JButton searchButton = new JButton("Search", Images.SEARCH); searchButton.setHorizontalTextPosition(SwingConstants.LEADING); ShowCodeAction showCode = Demo.getInstance().new ShowCodeAction(); showCode.setPage("Dynamic Query", "server/OpenBookServiceImpl.java.html#buildQuery"); JButton viewCodeButton = new JButton(showCode); JPanel buttonPanel = new JPanel(); buttonPanel.add(Box.createHorizontalGlue()); buttonPanel.add(searchButton); buttonPanel.add(Box.createHorizontalGlue()); buttonPanel.add(viewCodeButton); BoxLayout box = new BoxLayout(this, BoxLayout.Y_AXIS); setLayout(box); add(panel); add(Box.createVerticalGlue()); add(buttonPanel); _queryView.setBorder(BorderFactory.createTitledBorder("Criteria Query as CQL")); _queryView.setWrapStyleWord(true); _queryView.setEditable(false); _queryView.setBackground(getBackground()); add(_queryView); searchButton.addActionListener(this); } /** * Execute a query and displays the result onto {@linkplain SelectBookPanel}. * * The query is executed in a background, non-AWT thread. */ public void actionPerformed(ActionEvent e) { new SwingWorker<List<Book>, Void>() { private String queryString; @Override protected List<Book> doInBackground() throws Exception { queryString = _service.getQuery(_title.getText(), asDouble(_minPrice), asDouble(_maxPrice), _author.getText()); return _service.select(_title.getText(), asDouble(_minPrice), asDouble(_maxPrice), _author.getText(), (QueryDecorator[])null); } @Override protected void done() { try { _queryView.setText(_formatter.prettyPrint(queryString).toString()); List<Book> selectedBooks = get(1, TimeUnit.SECONDS); _selectPanel.updateDataModel(selectedBooks); } catch (Exception e) { new ErrorDialog(e).setVisible(true); } } }.execute(); } boolean isEmpty(JTextField text) { if (text == null) return true; String s = text.getText(); return s == null || s.isEmpty() || s.trim().isEmpty(); } Double asDouble(JTextField field) { if (isEmpty(field)) return null; try { return Double.parseDouble(field.getText()); } catch (NumberFormatException e) { System.err.println("Can not convert [" + field.getText() + "] to a double"); } return null; } } /** * A panel to display the selected books in a tabular format. * * @author Pinaki Poddar * */ class SelectBookPanel extends JPanel { private final JLabel _bookCount; private final EntityTableView<Book> _selectedBooks; SelectBookPanel(String title) { setLayout(new BorderLayout()); setBorder(BorderFactory.createTitledBorder(title)); _selectedBooks = new EntityTableView<Book>(Book.class, EntityDataModel.BASIC_ATTR | EntityDataModel.ROW_COUNT, _service.getUnit()); _bookCount = new JLabel(); add(_bookCount, BorderLayout.NORTH); add(_selectedBooks, BorderLayout.CENTER); } void updateDataModel(List<Book> books) { _bookCount.setText(books.size() + " Book selected"); _selectedBooks.getDataModel().updateData(books); } } /** * A panel to display the details of a single book and a button to add the * book to cart. Listens to the selection in the selected books. * * @author Pinaki Poddar * */ class BuyPanel extends JPanel implements ListSelectionListener { JLabel _bookTitle; JLabel _bookAuthors; JLabel _bookPrice; JLabel _bookISBN; JButton _addToCart; JSpinner _quantity; JPanel _quantityPanel; public BuyPanel(String title) { super(true); setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); setBorder(BorderFactory.createTitledBorder(title)); JPanel descPanel = new JPanel(); descPanel.setLayout(new BoxLayout(descPanel, BoxLayout.Y_AXIS)); _bookTitle = new JLabel(); _bookAuthors = new JLabel(); _bookPrice = new JLabel(); _bookISBN = new JLabel(); descPanel.add(_bookTitle); descPanel.add(_bookAuthors); descPanel.add(_bookPrice); descPanel.add(_bookISBN); add(descPanel); _quantityPanel = new JPanel(); SpinnerNumberModel spinnerModel = new SpinnerNumberModel(1, 1, 10, 1); _quantity = new JSpinner(spinnerModel); _quantityPanel.add(new JLabel("Quantity:")); _quantity.setEnabled(false); _quantityPanel.add(_quantity); _quantityPanel.setVisible(false); add(_quantityPanel); add(Box.createVerticalGlue()); JPanel buttonPanel = new JPanel(); buttonPanel.add(Box.createHorizontalGlue()); _addToCart = new JButton("Add to Cart", Images.CART); _addToCart.setEnabled(false); buttonPanel.add(_addToCart); buttonPanel.add(Box.createHorizontalGlue()); add(buttonPanel); _addToCart.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { _cartPanel.addBook((Book)_addToCart.getClientProperty("Book"), (Integer)_quantity.getValue()); } }); } void showBookDetails(Book book) { _bookTitle.setText(book.getTitle()); List<Author> authors = book.getAuthors(); if (authors != null && !authors.isEmpty()) { StringBuilder names = new StringBuilder(); for (Author author : authors) { if (names.length() != 0) names.append(", "); names.append(author.getName()); } _bookAuthors.setText("by " + names); } _bookPrice.setText("Price:" + book.getPrice()); _bookISBN.setText("ISBN: " + book.getISBN()); _addToCart.setEnabled(true); _quantity.setEnabled(true); _quantityPanel.setVisible(true); _addToCart.putClientProperty("Book", book); } @Override public void valueChanged(ListSelectionEvent e) { EntityTable<Book> table = _selectPanel._selectedBooks.getTable(); int row = table.getSelectedRow(); if (row == -1) return; int col = table.getSelectedColumn(); if (col == -1) return; EntityDataModel<Book> model = (EntityDataModel<Book>) table.getModel(); Book book = model.getRow(row); showBookDetails(book); } } /** * A panel to display the shopping cart. * * @author Pinaki Poddar * */ class ShoppingCartPanel extends JPanel implements ActionListener { private static final int MAX_ITEMS = 10; private final ShoppingCart _cart; private final JButton _placeOrder; private final JLabel[] _items = new JLabel[MAX_ITEMS]; public ShoppingCartPanel(String title) { _cart = _customer.newCart(); setLayout(new BoxLayout(this,BoxLayout.Y_AXIS)); setBorder(BorderFactory.createTitledBorder(title)); _placeOrder = new JButton("Place Order", Images.START); _placeOrder.setHorizontalTextPosition(SwingConstants.LEADING); _placeOrder.setEnabled(false); for (int i = 0; i < MAX_ITEMS; i++) { _items[i] = new JLabel(""); add(_items[i]); } add(Box.createVerticalGlue()); JPanel buttonPanel = new JPanel(); buttonPanel.add(Box.createHorizontalGlue()); buttonPanel.add(_placeOrder); buttonPanel.add(Box.createHorizontalGlue()); add(buttonPanel); _placeOrder.addActionListener(this); } /** * Add the given book to the cart. Updates the display. */ public void addBook(Book book, int quantity) { _cart.addItem(book, quantity); updateDisplay(); } void updateDisplay() { Map<Book, Integer> items = _cart.getItems(); int i = 0; for (Map.Entry<Book, Integer> entry : items.entrySet()) { JLabel item = _items[i++]; int quantity = entry.getValue(); Book book = entry.getKey(); item.setText(quantity + (quantity == 1 ? " copy of " : " copies of ") + book.getTitle()); item.setIcon(Images.DONE); } _placeOrder.setEnabled(items.size()>0); super.repaint(); } @Override public void actionPerformed(ActionEvent e) { new SwingWorker<PurchaseOrder, Void>() { @Override protected PurchaseOrder doInBackground() throws Exception { return _service.placeOrder(_cart); } @Override protected void done() { try { get(1, TimeUnit.SECONDS); _cart.clear(); for (int i = 0; i < MAX_ITEMS; i++) { _items[i].setText(""); _items[i].setIcon(null); } _placeOrder.setEnabled(false); } catch (Exception e) { new ErrorDialog(e).setVisible(true); } } }.execute(); } } }