суббота, 4 февраля 2012 г.

Клиент-серверные приложения на Java. Реализация консольного магазина с помощью баз данных.

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


    Итак, структура магазина пока остаётся прежней:

 Start page.
       |          |           |
       |          |           |
       |          |           |
 catalogs    |           |
    |           basket    |
    |                          home page
    |__bikes
    |             |__Kinesis Racelite KR-210
    |
    |__skates
    |             |__ Schankel Rebec
    |
    |__bike wheels
    |                       |__Mavic 28 inches
    |
    |__skate wheels
    |                       |__Powerslide Impact 110mm
    |                       |__Matter Juice F1110
    |                       |__MPC Black Track EX-FIRM
    |                       |__Inlinebus 110mm
    |                       |__Rollerblade 100mm
    |__ skinsuits
                       |__Schankel
                       |__Rollerblade



Но теперь мы составим базу данных под названием goods для хранения наших каталогов товаров.

Схема БД будет иметь такой вид:




  Первичными ключами в ней будут первые сверху строки - id.

Для простоты используем распространённую и несложную СУБД MySQL, модуль которой можно интегрировать в IDE.

Теперь создадим собственно базу данных. Я пользовался для этого встроенными возможностями IDE NetBeans. Выбираем вкладку "Службы", в ней - "Сервер MySQL в localhost", правый клик на этой вкладке, далее - "Создать базу данных"=> задаём имя базы данных=>"Готово".

  Теперь у нас есть база данных. Но ещё рано прыгать от радости, она пустая. В неё нужно добавить нужные таблицы, в таблицы нужные столбцы, а потом - нужные строки, собственно, те данные, которые в этой базе будут храниться.

   Для подключения к базе данных, кликаем правой кнопкой на пункт "goods" в выпадающем меню "Сервер MySQL в localhost", выбираем "Установить соединение". После кратких раздумий, СУБД установит с ней соединение, после чего, переместимся к соответствующему подключению:

jdbc:mysql://localhost:3306/goods [root на Схема по умолчанию]

Откроем выпадающее меню этого пункта, правым кликом на вкладке "Таблицы", выберем "Создать таблицу". Создадим таблицы с именами bike_wheels, bikes, catalogs, skate_wheels, skates, skinsuits, задав каждой таблице соответствующие схеме столбцы (тип столбца выберем VARCHAR длиной 30 знаков). При этом, первый столбец - id во всех таблицах будет первичным ключом.После чего, заполняем каждую таблицу данными.
    Правым кликом на таблице catalogs выберем пункт "Выполнить команду". Появится диалоговое окно для ввода SQL-запроса. Далее, как нравится: либо делаем текстовый документ и загружаем его запросом
LOAD DATA INFILE с соответствующим синтаксисом, либо, загружаем данные построчно. Я использовал второй, более простой способ. В поле запроса вводим:

 INSERT INTO catalogs (number, name) VALUES ('1', 'Skates')

Далее, ещё четыре запроса для категорий товаров Bikes, Bike wheels, Skate Wheels, Skinsuits.
     После чего, повторяем действия для таблиц skates, bike_wheels, bikes,  skate_wheels, skates, skinsuits, заполняя их соответствующими наименованиями товаров, описанием, ценой согласно схеме базы данных.


   Итак, нужные разделы БД созданы. Теперь нужно создать слой управления БД и через него заполнить таблицы данными. Он же будет потом использоваться для извлечения данных из таблицы.

   Для этого в среде NetBeans создадим новый проект, выбрав соответствующий пункт в меню "Файл". В категориях проекта выбираем "Приложение Java", жмём "Далее", выбираем имя проекта, снимаем галочку с пункта "Создать главный класс", жмём "Готово".

  Теперь у нас есть заготовка проекта Java и заготовка базы данных, из которых мы и будем делать наше клиент-серверное приложение. Далее, клик правой кнопкой мыши на пакет по умолчанию нашего проекта, выбираем "Создать">"Мастер конфигурации Hibernate". Задаём имя файла по умолчанию, жмём "Далее", выбираем подключение к нашей базе данных "goods", и жмём "Готово". Мы создали файл конфигурации Hibernate. Он представляет из себя XML-документ, в котором содержатся свойства подключения к базе данных, а также, данные пользователя, от которого система будет впоследствии обращаться к БД. Позже в него добавятся мэппинги на соответствующие таблицы нашей БД, через которые и будут происходить соответствующие запросы к ним.

  Далее, создадим новый пакет исходных файлов goods.util, в котором создадим файл HibernateUtil с помощью мастера создания этого файла. Оставим содержимое файла по умолчанию.

В нашем случае оно будет выглядеть так:



package goods.util;

import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.SessionFactory;

/**
 * Hibernate Utility class with a convenient method to get Session Factory object.
 *
 * @author stas
 */
public class NewHibernateUtil {

    private static final SessionFactory sessionFactory;
   
    static {
        try {
            // Create the SessionFactory from standard (hibernate.cfg.xml) 
            // config file.
            sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            // Log the exception. 
            System.err.println("Initial SessionFactory creation failed." + ex);
            throw new ExceptionInInitializerError(ex);
        }
    }
   
    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }
}

Этот файл создаёт сессию, через которую и будет происходить связь проекта с базами данных.

Для непосредственной связи с сущностями таблиц, в Hibernate нужно создать ещё один файл, так называемого "обратного проектирования". Именно он и будет решать, какие таблицы нашей БД включить в проект. Создаётся он в пакете по умолчанию, рядом с основным конфигурационным файлом аналогичным ему образом выбора из меню мастера создания файлов Hibernate. В процессе создания нам будет предложено выбрать, какие таблицы базы данных использовать. Выбираем все. Жмём "Готово". Всё, у нас есть мастер обратного проектирования. Выглядеть он должен примерно так:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-reverse-engineering PUBLIC "-//Hibernate/Hibernate Reverse Engineering DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-reverse-engineering-3.0.dtd">
<hibernate-reverse-engineering>
  <schema-selection match-catalog="goods"/>
  <table-filter match-name="skates"/>
  <table-filter match-name="bikes"/>
  <table-filter match-name="bike_wheels"/>
  <table-filter match-name="skate_wheels"/>
  <table-filter match-name="skinsuits"/>
  <table-filter match-name="catalogs"/>
</hibernate-reverse-engineering> 

  Далее, нам нужно создать xml-файлы сопоставления и файлы POJO (Plain Old Java Objects) для каждой таблицы. Они и будут теми классами, непосредственно к которым будет обращаться код для получения доступа к БД. Для них создадим ещё один пакет исходных файлов в проекте, назвав его goods.entity. В нём, в свою очередь, вызовем мастер создания файлов, выберем категорию "Hibernate" и пункт "Файлы размещения Hibernate и объекты POJO из базы данных". В аргументах выбираем созданный ранее файл обратного проектирования.
  Всё, основа для доступа к базам данных у нас готова. Теперь можно провести тестовые запросы на языке HQL (Hibernate Query Language), чтобы убедиться, что слой работоспособен. Для этого в меню правой кнопки мыши файла hibernate.cfg.xml текущего проекта выбираем пункт "Выполнить запрос HQL". HQL - это упрощённый язык запросов. Запрос, эквивалентный
SELECT * FROM skates на HQL будет выглядеть таким образом:

from Skates

Где Skates будет сущностью, представленной объектом POJO, которая ссылается на определённую таблицу базы данных.
 В результате, программа выведет табличку, где в соответствующих колонках будет записано:

900 USD    Schankel Rebec    Chinese noname ABEC7    Powerslide Impact    011    Luigino P-51 Pilot 4x110

Аналогичные запросы можно провести для остальных таблиц базы данных, чтобы убедиться, что сущности сгенерированы правильно, мэппинги в конфигурационных файлах расставлены корректно.
Кстати, файл конфигурации, если после создания сущностей он был отредактирован правильно, должен выглядеть вот так:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/goods</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password">*********</property>
    <mapping resource="goods/entity/Skates.hbm.xml"/>
    <mapping resource="goods/entity/Skinsuits.hbm.xml"/>
    <mapping resource="goods/entity/SkateWheels.hbm.xml"/>
    <mapping resource="goods/entity/Bikes.hbm.xml"/>
    <mapping resource="goods/entity/BikeWheels.hbm.xml"/>
    <mapping resource="goods/entity/Catalogs.hbm.xml"/>
  </session-factory>
</hibernate-configuration> 
 Где звёздочками обозначается пароль пользователя, от имени которого 
система будет обращаться к базе данных.После того, как слой баз данных
собран, можно приступить непосредственно к разработке консольного
приложения. Для этого создадим класс-сокет в новом пакете исходных
файлов goods.main. Начало его будет аналогично классу-сокету из
прошлого примера:

import goods.main.Server;
import java.net.*;
import java.io.*;
import java.util.*;

public class Server1 implements Runnable{

static ServerSocket ss;
       Socket s;
       Thread t;
       byte [] data;
       static String buf="";
       String sbuf="";
       static String out;
       static List two;
       static Map basket=new HashMap();
       static Map payment=new HashMap();
       static int ab=0;
       //static Map basket;
       static String temp=" ";
  // Declarating the socket parameters
    PrintStream os;
    InputStreamReader dis;
    DataOutputStream dos;
    boolean stop = false;
    BufferedReader brr=new BufferedReader(new InputStreamReader(System.in)) ;
    Server1()throws Exception  //Constructor. Further it is engaged for "eternal" loop for connections awaiting
    { System.out.println("Message received!");
        s=ss.accept();

        dis = new InputStreamReader(s.getInputStream());
        dos = new DataOutputStream(s.getOutputStream());
        t= new Thread(this);
        t.start(); // Stream startup
System.out.println("Message received!");


    }

    public void run ()// Stream body

    {
        try
        {
                   do
                   {

                       if(dis.ready())
                       {
                    System.out.println("Message received!");       
                      while(dis.ready())
// Here we read the data from the client
                      {
                          Character c =(char)dis.read();
                          buf=buf+c.toString();

                      }

        
                       List two=new ArrayList();
    String a=buf;
    StringTokenizer st = new StringTokenizer(a);

     while (st.hasMoreTokens()) {
         two.add(st.nextToken());
    } // here we identificate the input search parameters such as depth and mask
    String mask=two.get(two.size()-1).toString();
    Server1.out=" ";
if(mask.contains("m")&&mask.length()<2){
    
String out="Hi, it's my Stas Network Shop! \n - enter 'c' to view the catalogs \n - enter 'b' to view your basket \n - enter 'h' to view my home page";

                           
                       
         os = new PrintStream(s.getOutputStream());
                     os.println(out);
                           }

                       // Here the program initializes the search class

                      
                      
                      Server s=new Server(); 

 А вот дальше нам нужно прописать реакции сервера на ввод пользователем
запросов на открытие каталогов, карточек товаров, добавление товаров в корзину.
В общем-то, задача аналогичная прошлому примеру, только теперь сервер должен
обращаться не к структурам данных типа Map, в которых содержались интересующие
пользователя данные, а к слою баз данных, в которых эти данные теперь хранятся.
Для передачи HQL запросов из кода программы, а также, сохранения результатов
запросов в промежуточных структурах данных, создадим ещё один класс в этом же
пакете, назовём его просто Server.
В этом классе нам понадобятся следующие библиотеки и поля:
package goods.main;

import goods.entity.Bikes;
import goods.entity.Catalogs;
import goods.entity.SkateWheels;
import goods.util.NewHibernateUtil;
import goods.entity.Skates;
import goods.entity.Skinsuits;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Vector;
import org.hibernate.Query;
import org.hibernate.HibernateException;
import org.hibernate.Session;

/**
 *
 * @author stas
 */

public class Server {
    public BufferedReader brr=new BufferedReader(new InputStreamReader(System.in)) ;
static String subresult="";    
private static String FIRST_REQUEST="from Skates";
static String result;
public static Query q;
static List resultList;
public static boolean shortt;
public static int a031=0;

   Они понадобятся в дальнешем. Теперь, напишем метод, собственно, совершающий запрос к базе данных, используя входной параметр hql типа String и возвращающий результат этого запроса:

@SuppressWarnings("unchecked")
    public static String executeHQLQuery(String hql) {
    try {
        
        Session session = NewHibernateUtil.getSessionFactory().openSession();
        session.beginTransaction();
       
         q = session.createQuery(hql);
        resultList = q.list();
        result=displayResult(resultList);
        session.getTransaction().commit();
       
        
    } catch (HibernateException he) {
        he.printStackTrace();
        
    }
    return result; 
}
   С отправкой запроса, надеюсь, всё ясно, ей занимается метод createQuery(), использующий в качестве входного аргумента нашу переменную hql.
   А вот с выводом результатов запроса... Как видно из кода, ExecuteHQLQuery(hql), для получения результата использует таинственный метод displayResult(). Этот метод, в отличие от createQuery(), не определён по умолчанию, его нужно создать. Он представляет из себя несколько структур данных, взаимодействующих между собой для передачи и преобразования в удобный для пользователя вид данных, полученных по запросу из БД.
     Для начала, создадим в методе displayResult() 2 динамически расширяемых коллекции типа Vector():

private static String displayResult(List resultList) {
     
            Vector<String> skateHeaders = new Vector<String>();
    Vector tableData = new Vector();
    skateHeaders.add("Id"); 
    skateHeaders.add("Boot");
    skateHeaders.add("Frame");
    skateHeaders.add("Bearings");
    skateHeaders.add("Wheels");
    skateHeaders.add("Price");
    
    Vector<String> bikeHeaders=new Vector<String>();
    bikeHeaders.add("Bike id");
    bikeHeaders.add("Frame");
    bikeHeaders.add("Fork");
    bikeHeaders.add("Name");
    bikeHeaders.add("Pedals");
    bikeHeaders.add("Saddle");
    bikeHeaders.add("Shifters");
    bikeHeaders.add("Wheels");
    bikeHeaders.add("Price");

   В них будут храниться заголовки спецификаций велосипедов и роликов.
По задумке, пользователь должен вводить предлагаемые ему номера артикулов товаров для чтения их спецификаций. Для этого в файле Server1 пропишем такое условие:

Server s=new Server();
                       if(mask.contains("011")&&mask.length()<4){ 
                           s.shortt=false;
                       Server1.out=s.executeHQLQuery("from Skates where id=011");
                           }

   То есть, если введённые пользователем данные равны артикулу "011", производится HQL-запрос на соответствующую позицию к базе данных. Слою базы данных неаобходимо передать этот запрос, получить и сохранить его результаты, добавить к ним заголовки, указанные в двух структурах типа Vector выше и, наконец, отправить всё это пользователю. Для этого в файле Server, который является слоем взаимодействия с базами данных, пропишем соответствующую реакцию на запрос "from Skates where id=011":

for(Object o : resultList) {
          if (q.getQueryString().contains("011")){
              subresult="";
        Skates skates = (Skates)o;
        Vector<Object> oneRow = new Vector<Object>();
        oneRow.add(skates.getId());
        oneRow.add(skates.getBoot());
        oneRow.add(skates.getFrame());
        oneRow.add(skates.getBearings());
        oneRow.add(skates.getWheels());
        oneRow.add(skates.getPrice());
        
        for(int i=0; i<oneRow.size();i++)
           subresult=subresult+skateHeaders.get(i).toString()+": "+oneRow.get(i).toString()+","+"\n";        
        subresult=subresult.replace(",", "\n").replace("[", "").replace("]", "")+"\n"+"Enter 'c' to return to catalogs"+"\n"+"Enter 's' to add item to the basket"+"\n"+"Enter 'm' to return to main page"+"\n"+"Enter 'b' to view your basket";

          }

А поскольку метод displayResult() возвращает строку subresult, он и вернёт на вывод в строковом виде ответ сервера:


Id: 011

Boot: Schankel Rebec

Frame: Luigino P-51 Pilot 4x110

Bearings: Chinese noname ABEC7

Wheels: Powerslide Impact

Price: 900 USD


Enter 'c' to return to catalogs
Enter 's' to add item to the basket
Enter 'm' to return to main page
Enter 'b' to view your basket

Аналогичным образом организуем запросы для  остальных категорий товаров и для каталогов товаров.
  Кроме того, нам нужно организовать добавление товаров в корзину при вводе пользователем 's' и просмотр содержимого корзины при вводе 'b' с подсчётом суммарной стоимости покупки. Для этого, введём статическое целочисленное поле a031 в класс Server:

public static int a031=0;


 и будем в каждом запросе присваивать ему целочисленное значение цены, извлечённое из ячейки Price соответствующего товара:

if (q.getQueryString().contains("033")&(true==shortt)){
                        subresult="";
                        tableData.clear();
                        SkateWheels sws = (SkateWheels)o;
                        Vector<Object> oneRow = new Vector<Object>();  
                        oneRow.add(sws.getIdWheel());
                        oneRow.add(sws.getNameWheel());
                        oneRow.add(sws.getDiameter());
                        oneRow.add(sws.getPrice());
                        a031=Integer.parseInt(sws.getPrice().toString().replace(" USD", ""));
                        tableData.add(oneRow);  
                         subresult=subresult+tableData.get(0);
                         subresult=subresult.replace(",", "\n").replace("[", "").replace("]", "");
                    }

Переменная shortt - ещё одно статическое поле класса Server, на этот раз не численное, а булево.  Его значение, присваиваемое при запросе, будет определять, полную, или сокращённую информацию о товаре выводить. Полная информация нам нужна при просмотре спецификации карточки товара. В этом случае, мы в классе Server1 присвоим false статическому полю shortt экземпляра класса Server и получим в выводе полный список данных о товаре. В результате, в слое базы данных запрос будет направлен на этот блок:

if (q.getQueryString().contains("033")&(false==shortt)){
                        subresult="";
                        tableData.clear();
                        SkateWheels sws = (SkateWheels)o;
                        Vector<Object> oneRow = new Vector<Object>();  
                        oneRow.add(sws.getIdWheel());
                        oneRow.add(sws.getNameWheel());
                        oneRow.add(sws.getDiameter());
                        oneRow.add(sws.getHardness());
                        oneRow.add(sws.getCond());
                        oneRow.add(sws.getPrice());
                        tableData.add(oneRow);  
                         subresult=subresult+tableData.get(0);
                         subresult=subresult.replace(",", "\n").replace("[", "").replace("]", "")+"\n"+"Enter 'c' to erturn to catalogs"+"\n"+"Enter 's' to add item to the basket"+"\n"+"Enter 'm' to return to main page"+"\n"+"Enter 'b' to view your basket";
                    }


   И цена товара подсчитываться не будет.
   В противном случае, если мы хотим отобразить товар в каталоге, или в корзине, нам нужны только его основные параметры: артикул, наименование и цена и мы присвоим полю shortt значение true.
   Наконец, пропишем логику корзины. Для начала, нужно прописать реакции на добавление пользователем товара в корзину (ввод пользователем 's'). Добавление товара в корзину возможно только со страницы спецификации товара. Поэтому, воспользуемся предыдущим значением, введённым пользователем, которое и будет номером артикула (значение это мы возьмём из коллекции two, служащей для хранения и передачи введённых значений, рассмотренной ещё в предыдущем посте) :

if(mask.contains("s")&&mask.length()<2){
                     s.shortt=true;
 basket.put(basket.size()+1, two.get(two.size()-2).toString());
               if((two.get(two.size()-2).toString().startsWith("01"))){                  
                    ab=ab+s.a031;                
                   temp=temp+" "+basket.get(basket.size()).toString()+s.executeHQLQuery("from Skates where id="+two.get(two.size()-2).toString());   
      
               }
              if((two.get(two.size()-2).toString().contains("03"))){
                  s.shortt=true;
                 String qq=s.executeHQLQuery("from SkateWheels where id_wheel="+two.get(two.size()-2).toString());
               ab=ab+s.a031;
              temp=temp+" "+basket.get(basket.size()).toString()+s.executeHQLQuery("from SkateWheels where id_wheel="+two.get(two.size()-2).toString()); 
              }
                    if((two.get(two.size()-2).toString().startsWith("00"))){
                  s.shortt=true;
                 String qq=s.executeHQLQuery("from Bikes where bike_id="+two.get(two.size()-2).toString());
               ab=ab+s.a031;
              temp=temp+" "+basket.get(basket.size()).toString()+s.executeHQLQuery("from SkateWheels where id_wheel="+two.get(two.size()-2).toString()); 
                    }
                    if((two.get(two.size()-2).toString().startsWith("04"))){
                  s.shortt=true;
                 String qq=s.executeHQLQuery("from Skinsuits where id="+two.get(two.size()-2).toString());
               ab=ab+s.a031;
              temp=temp+" "+basket.get(basket.size()).toString()+s.executeHQLQuery("from SkateWheels where id_wheel="+two.get(two.size()-2).toString()); 
                    
              Server1.out=temp+"\n"+"Total Price: "+ab;
           os.println(Server1.out);
           }
                 }
В этом блоке прописаны запросы, в зависимости от каталога, к которому товар относится. Каталог определяется по 2-м первым цифрам номера артикула, путём использования метода startsWith(String char) класса String.Также, производится подсчёт стоимости покупки, добавление его в суммарную стоимость заказа и вывод на экран содержимого корзины и общей стоимости заказа, так же, как и стоимости каждого наименования отдельно.
Кроме того, можно просто посмотреть содержимое корзины, ничего при просмотре в неё не добавляя. Для этого пользователь может ввести 'b'. реакция на это событие прописывается таким образом:

if(mask.contains("b")&&mask.length()<2){
                          s.shortt=true;
            temp="";
           for(int i=1;i<=basket.size();i++){
              if(basket.get(i).toString().startsWith("01")){                  
              temp=temp+" "+basket.get(i).toString()+s.executeHQLQuery("from Skates where id="+basket.get(i).toString());
              
              }
              if(basket.get(i).toString().startsWith("03")){
                   
              temp=temp+" "+basket.get(i).toString()+s.executeHQLQuery("from SkateWheels where id_wheel="+basket.get(i).toString()); 
              }
              if(basket.get(i).toString().startsWith("00")){                  
              temp=temp+" "+basket.get(i).toString()+s.executeHQLQuery("from Bikes where bike_id="+basket.get(i).toString());
              
              }
              if(basket.get(i).toString().startsWith("04")){                  
              temp=temp+" "+basket.get(i).toString()+s.executeHQLQuery("from Skinsuits where id="+basket.get(i).toString());             
              }
              }
              
           Server1.out=temp+"\n"+"Total Price: "+ab;
           os.println(Server1.out);
           }

Таким образом, при выборе роликов с артикулом 011, добавлении их в корзину, переходе к каталогам, выборе колёс для роликов с артикулом 033 и также добавлением их в корзину, пользователь вводит в консоль 'b' с целью просмотра содержимого корзины и получает такой ответ от сервера:

b

011Id: 011

Boot: Schankel Rebec

Frame: Luigino P-51 Pilot 4x110

Bearings: Chinese noname ABEC7

Wheels: Powerslide Impact

Price: 900 USD


Enter 'c' to return to catalogs
Enter 's' to add item to the basket
Enter 'm' to return to main page
Enter 'b' to view your basket 033033
MPC Black Track
110 mm
160 USD
Total Price: 1060

Дальше можно было бы написать предложение перейти к оплате, а также, прописать перенаправление к PayPal, автоматическую отправку письма на почту администратора, но такие тонкости ,необходимые для работы магазина в реальном мире уже стоит приберечь для полноценной веб-версии электронного магазина.
Вот как-то так. Честно говоря, до выполнения этого задания мне казалось, что логика интернет-магазинов довольно проста. И только здесь мне стала понятна её сложность, особенно, при реализации в Java. Та сложность, которую видит пользователь - это просто верхушка умело спрятанного программистом айсберга кода.