СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

H Пошаговая разработка веб-приложения в черновиках Из песочницы Tutorial

При устройстве на работу java программистом меня попросили написать тестовое web приложение «Телефонный справочник». Хочу поделиться с вами этим «шедевром».

Вид и функциональность приложения


  • Добавление;
  • Удаление;
  • Поиск;
  • Валидация данных.









Инструменты


  1. IntelliJ IDEA 13 скачать
  2. Ext JS 4.2 скачать
  3. Apache Tomcat 7.0.55 скачать
  4. MySQL 5.6.20 скачать
  5. Apache Maven 3.0.5 скачать
  6. Java 1.6.0_65 скачать
  7. Hibernate читать
  8. JPA читать
  9. Spring читать
  10. SQL читать
  11. MVC читать
  12. DAO читать
  13. Layer Service читать


Создание проекта




Укажите путь к Java в Project SDK.




Укажите путь к своей Maven home directory.



«Maven projects need to be imported» кликаем Enable Auto-Import.



Добавим Tomcat Server.





В Application server укажите путь до Tomcat сервера.




Ok -> Apply -> Ok

Проверим, что всё работает.





Клиент (ExtJS)


Добавим файлы фрэймворка.



Модель ExtJS MVC.



Создадим файл app.js



app.js
Ext.application({           
    name: 'PhonesDir',   
    appFolder: 'app',
    launch: function () {
        Ext.create('Ext.container.Viewport', {
            layout: 'fit',
            items: {
                xtype: 'panel',
                html: '<h2>Телефонный справочник</h2>'
            }
        });
    }
});


  • Метод Ext.application инициализирует приложение Ext JS;
  • name: 'PhonesDir' указывает имя приложения, которое будет затем использоваться для создания полных имен классов приложения;
  • appFolder: 'app' указывает на нашу папку с инфраструктурой приложения (controller, model, store, view);
  • launch: function(){} тут происходит создание приложения.


В файле index.jsp подключаем стили ExtJS, затем фреймворк ExtJS и только потом app.js:

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Справочник</title>
    <link rel="stylesheet" type="text/css" href="resources/css/ext-all.css"/>
    <script type="text/javascript" src="resources/ext-all.js"></script>
    <script type="text/javascript" src="app.js"></script>
</head>
<body>
</body>
</html>


Проверим, что всё работает.



View

Нам понадобится четыре вида — это вид поиска SearchPhones.js, вид таблицы PhoneGrid.js, вид формы добавления данных AddWindowForm.js и вид каркаса PhonesDirectory.js, куда мы поместим все виды.



SerachPhones.js

Ext.define('PhonesDir.view.SearchPhones', {
    extend: 'Ext.form.Panel',
    alias: 'widget.searchPhones',
    bodyPadding: 10,
    items: [
        {
            xtype: 'textfield',
            name: 'search',
            fieldLabel: 'Введите имя',
            maxLength: 200
        }
    ]
});


PhoneGrid.js

Ext.define('PhonesDir.view.PhonesGrid', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.phonesGrid',
    width: 400,
    height: 300,
    frame: true,
    iconCls: 'icon-user',
    columns: [
        {
            text: 'Имя',
            flex: 1,
            sortable: true,
            dataIndex: 'name'

        },
        {
            flex: 2,
            text: 'Телефон',
            sortable: true,
            dataIndex: 'phone'
        }
    ],
    dockedItems: [
        {
            xtype: 'toolbar',
            items: [
                {
                    text: 'Добавить',
                    action: 'add',
                    iconCls: 'icon-add'
                },
                '-',
                {
                    action: 'delete',
                    text: 'Удалить',
                    iconCls: 'icon-delete',
                    disabled: true
                }
            ]
        }
    ]
});


AddWindowForm.js

Ext.define('PhonesDir.view.AddWindowForm', {
    extend: 'Ext.window.Window',
    alias: 'widget.addWindowForm',
    autoShow: true,
    layout: 'fit',
    modal: true,

    items: [
        {
            bodyPadding: 10,
            xtype: 'form',
            items: [
                {
                    xtype: 'textfield',
                    name: 'name',
                    fieldLabel: 'Имя',
                    regex: /^[А-Я-Ё][а-я-ё]{1,}$/,
                    regexText: 'Имя должно состоять из двух и более букв и начинаться с заглавной буквы',
                    allowBlank: false,
                    blankText: 'Это поле должно быть заполнено'
                },
                {
                    xtype: 'textfield',
                    name: 'phone',
                    fieldLabel: 'Телефон',
                    regex: /^([+][0-9]{11,12})*$/,
                    regexText: 'Телефон должно соответсвовать шаблону "+XXXXXXXXXXX" или "+XXXXXXXXXXXX", где X - цифры. ',
                    allowBlank: false,
                    blankText: 'Это поле должно быть заполнено'
                }
            ]
        }
    ],

    buttons: [
        {
            text: 'Сохранить',
            action: 'save',
            disabled: true
        },
        {
            text: 'Отменить',
            handler: function () {
                this.up('window').close();
            }

        }
    ]
});


PhonesDirectory.js

Ext.define('PhonesDir.view.PhonesDirectory', {
    extend: 'Ext.panel.Panel',
    width: 500,
    height: 360,
    padding: 10,
    alias: 'widget.phonesDirectory',
    layout: 'border',
    items: [
        {
            xtype: 'phonesGrid',
            region: 'center'
        },
        {
            xtype: 'panel',
            html: '<div style="font: normal 18px cursive"><center><font size = "10">Телефонный справочник</font></center></div>',
            region: 'north',
            height: 80
        },
        {
            xtype: 'searchPhones',
            title: 'Поиск',
            region: 'west',
            width: 300,
            collapsible: true,
            collapsed: false
        }
    ],
    renderTo: Ext.getBody()
});


Изменим параметр items в app.js.

     items: {
                xtype: 'phonesDirectory'
            }

  • phonesDirectory' алиас, который мы указали в параметре alias в PhoneDirectory.js.
  • Метод Ext.define('Имя', {параметры}) создает класс-компонент, который может быть унаследован от какого-нибудь компонента. Например в PhoneGrid.js указали extend: 'Ext.grid.Panel', что будет представлять собой таблицу.


Controller

Виды загружаются из контролера, поэтому создадим контролер PhonesDirectoryControoler.js и укажим его в app.js



PhonesDirectoryController.js
Ext.define('PhonesDir.controller.PhonesDirectoryController', {
    extend: 'Ext.app.Controller',

    views: [
        'AddWindowForm',
        'PhonesDirectory',
        'PhonesGrid',
        'SearchPhones'
    ],

    init: function () {
        this.control({

        });
    }


  • С помощью параметра init инициализируются обработчики для компонентов (кнопки, поля и т.д). Связать конпонент с обработчиком помогает функция control.


Конечная структура app.js

Ext.application({
    name: 'PhonesDir',

    appFolder: 'app',

    controllers: [
        'PhonesDirectoryController'
    ],

    launch: function () {
        Ext.create('Ext.container.Viewport', {
            layout: 'fit',
            items: {
                xtype: 'phonesDirectory'
            }
        });
    }
});


Проверим, что всё работает.



Модель и хранилище



PhonesDirectoryModel.js

Ext.define('PhonesDir.model.PhonesDirectoryModel', {
    extend: 'Ext.data.Model',
    fields: ['name', 'phone'],
    proxy: {
        pageParam: 'search',
        type: 'rest',
        api: {
            create: 'phone',
            read: 'phone',
            destroy: 'phone'
        },
        reader: {
            type: 'json',
            root: 'data',
            successProperty: 'success'
        },
        writer: {
            type: 'json',
            writeAllFields: true
        }

    }
});


  • pageParam: 'search' нужен для реализации логики поиска на сервере. В нём будут находится данные из поля поиска и, если не пусто, сервер выполнит выборку данных из БД, иначе вернет все данные.


PhonesDirectoryStore.js

Ext.define('PhonesDir.store.PhonesDirectoryStore', {
    extend: 'Ext.data.Store',
    model: 'PhonesDir.model.PhonesDirectoryModel',
    autoLoad: true,
    autoSync: true,
    proxy: {
        type: 'rest',
        api: {
            create: 'phone',
            read: 'phone',
            destroy: 'phone'
        },
        reader: {
            type: 'json',
            root: 'data',
            successProperty: 'success'
        },
        writer: {
            type: 'json',
            writeAllFields: true
        }

    }
});


  • 'phone' имя, на которое будет замапен java-класс (контролер), который будет обрабатывать GET, POST, DELETE запросы с клиента.


Добавим модель PhonesDirectoryModel.js и хранилище PhonesDirectoryStore.js в контролер PhonesDirectoryController.js



Добавим в PhonesGrid.js параметр store: 'PhonesDirectoryStore', для отображения данных в таблице.



Проверим, что всё работает. 404 (Not Found) — это нормально, так как по адресу localhost:8080/phone еще ничего нет.



Сервер (Java)


Добавьте зависимости в pom.xml
<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.jvm.version>1.7</project.jvm.version>
        <spring.version>3.2.2.RELEASE</spring.version>
        <spring.security>3.1.4.RELEASE</spring.security>
        <slf4j.version>1.5.6</slf4j.version>
        <log4j.version>1.2.17</log4j.version>
        <hibernate.version>4.2.2.Final</hibernate.version>
        <jackson.version>1.9.12</jackson.version>
        <lombok.version>0.11.8</lombok.version>
        <querydsl.version>3.2.0</querydsl.version>
        <springkex.version>0.0.23-SNAPSHOT</springkex.version>
    </properties>


    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>${hibernate.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.4</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>4.3.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-jpamodelgen</artifactId>
            <version>1.2.0.Final</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>${querydsl.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>${querydsl.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-core-asl</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-mapper-asl</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.3</version>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>${spring.security}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-ldap</artifactId>
            <version>${spring.security}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>${spring.security}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-taglibs</artifactId>
            <version>${spring.security}</version>
        </dependency>
        <dependency>
            <groupId>opensymphony</groupId>
            <artifactId>quartz</artifactId>
            <version>1.6.3</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.7.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.7.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
            <exclusions>
                <exclusion>
                    <artifactId>commons-pool</artifactId>
                    <groupId>commons-pool</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>com.jolbox</groupId>
            <artifactId>bonecp</artifactId>
            <version>0.7.1.RELEASE</version>
        </dependency>
    </dependencies>


Создадим папку java:



Создадим модель данных и слой доступа к данным (DAO)



Phone.java
@Entity
@Table(name = "phones")
public class Phone implements Serializable {

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Pattern(regexp = "^[А-Я-Ё][а-я-ё]{1,}$")
    @Column(name = "name")
    private String name;

    @Pattern(regexp = "^([+][0-9]{11,12})([,][+][0-9]{11,12})*$")
    @Column(name = "phone")
    private String phone;

    public Phone() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

}


PhoneDao.java

public interface PhoneDao {

    void add(Phone entity);

    void delete(Phone entity);

    Collection<Phone> getPhones(String search);

    public List findByPhone(String name, String phone);

}


PhoneDaoImpl.java

public class PhoneDaoImpl implements PhoneDao {

    @PersistenceContext
    private EntityManager emf;

    @Override
    public void add(Phone phone) {
        emf.persist(phone);
    }

    @Override
    public void delete(Phone phone) {
        emf.remove(emf.getReference(Phone.class, phone.getId()));
    }

    @Override
    public Collection<Phone> getPhones(String search) {
        if (null == search || search.trim().isEmpty()) {
            return emf.createQuery(
                    "select c from Phone c")
                    .getResultList();
        }
        return emf.createQuery(
                "select c from Phone c where c.name like :search")
                .setParameter("search", search.trim() + "%")
                .getResultList();
    }

    public List<Phone> findByPhone(String name, String phone) {
        return emf.createQuery(
                "select c from Phone c where c.name = :name and c.phone = :phone")
                .setParameter("name", name)
                .setParameter("phone", phone)
                .getResultList();
    }
}


  • Метод getPhones(String search) принимает значение параметра, которого мы указали в PhonesDirectoryModel.js;
  • Метод findByPhone(String name, String phone) используется для поиска дубликата при добавлении данных.


Создадим слой сервиса:



PhoneService.java

public interface PhoneService {

    Boolean add(Phone phone);

    Collection<Phone> getPhones(String search);

    void delete(Phone phone);

}


PhoneServiceImpl.java

public class PhoneServiceImpl implements PhoneService {

    private PhoneDao phoneDao;

    public PhoneDao getPhoneDao() {
        return phoneDao;
    }

    public void setPhoneDao(PhoneDao phonePhoneDao) {
        this.phoneDao = phonePhoneDao;
    }

    @Transactional
    @Override
    public Boolean add(Phone entity) {
        List<Phone> duplicate = phoneDao.findByPhone(entity.getName(), entity.getPhone());
        if (duplicate.isEmpty()) {
            phoneDao.add(entity);
            return true;
        }
        return false;
    }

    @Transactional
    @Override
    public Collection<Phone> getPhones(String search) {
        return phoneDao.getPhones(search);
    }

    @Transactional
    @Override
    public void delete(Phone entity) {
        phoneDao.delete(entity);
    }

}


Создадим контролер, который будет замапен на адрес /phone для обработки запросов с клиента.



PhoneController.java
@Controller
@RequestMapping("/phone")
public class PhoneController {

    @Autowired
    private PhoneService phoneService;


    @RequestMapping(method = RequestMethod.GET)
    @ResponseBody
    public Collection<Phone> getPhone(String search) {
        return phoneService.getPhones(search);
    }

    @RequestMapping(method = RequestMethod.POST)
    @ResponseBody
    public ExtResult setPhone(@RequestBody Phone phone) {
        return new ExtResult(phoneService.add(phone), phone);
    }

    @RequestMapping(value = "{id}", method = RequestMethod.DELETE)
    @ResponseBody
    public String delPhone(@RequestBody Phone phone) {
        phoneService.delete(phone);
        return "del";

    }
}


  • Каждый метод замапен на соответствующий запрос с клиента. Внедряем зависимость с помощью spring аннотации @Autowired и вызывает соответствующие методы у сервиса;
  • ExtResult — вспомогательный класс. Используется для ответа клиенту, что сущность, которую пытаемся записать в БД, дубликат или не дубликат.


ExtResult.java

public class ExtResult {
    private boolean success;
    private Phone data;


    public ExtResult(boolean success, Phone data) {
        this.success = success;
        this.data = data;
    }

    public ExtResult() {
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public Phone getData() {
        return data;
    }

    public void setData(Phone data) {
        this.data = data;
    }
}


Проверьте, что всё работает. Соберите проект с помощью maven install и запустите приложение.

Создадим spring контекст my-context.xml c:

  • настройками подключения к БД;
  • бином EntityManager — объект, через который происходит взаимодействие с БД. Инжектится в PhoneDaoImpl.java;
  • инжектом объекта класса PhoneDaoImpl.java в объект класса PhoneServiceImpl.java.




my-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:aop="http://www.springframework.org/schema/aop" xmlns:p="http://www.springframework.org/schema/p"
       xmlns:tx="http://www.springframework.org/schema/tx"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                           http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
          p:dataSource-ref="dataSource"
          p:packagesToScan="model"
          p:jpaProperties-ref="jpaProperties"
          p:persistenceProvider-ref="persistenceProvider"/>

    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"
          depends-on="entityManagerFactory"/>

    <bean id="dataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close"
          p:driverClass="com.mysql.jdbc.Driver"
          p:jdbcUrl="jdbc:mysql://localhost:3306/phonedir?characterEncoding=UTF-8"
          p:username="root"
          p:password="1234"
          p:idleConnectionTestPeriodInMinutes="1"
          p:idleMaxAgeInSeconds="240"
          p:minConnectionsPerPartition="2"
          p:maxConnectionsPerPartition="5"
          p:partitionCount="2"
          p:acquireIncrement="1"
          p:statementsCacheSize="100"
          p:releaseHelperThreads="2"
          p:statisticsEnabled="false"/>

    <bean id="persistenceProvider" class="org.hibernate.ejb.HibernatePersistence"/>

    <bean id="jpaProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="properties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="connection.pool_size">1</prop>
                <prop key="current_session_context_class">thread</prop>
                <prop key="show_sql">true</prop>
                <prop key="hibernate.hbm2ddl.auto">create</prop>
                <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
            </props>
        </property>
    </bean>
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"
          p:entityManagerFactory-ref="entityManagerFactory"/>

    <tx:annotation-driven transaction-manager="transactionManager"/>

    <bean name="phoneDao" class="model.dao.impl.PhoneDaoImpl">
    </bean>

    <bean name="PhoneService" class="service.impl.PhoneServiceImpl">
        <property name="phoneDao" ref="phoneDao">
        </property>
    </bean>

</beans>


Создайте БД с названием phonedir или измените название в контексте на своё.

Создадим настройки для spring DispatcherServlet, который будет обрабатывать запросы с клиента.



mvc-dispatcher-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="controllers"/>
    <mvc:view-controller path="/" view-name="/index.jsp"/>
    <mvc:resources mapping="/**" location="/"/>
    <mvc:annotation-driven/>

</beans>


  • context:component-scan поиск и регистрация компонентов в контейнере спринга;
  • mvc:view-controller домашняя страница;
  • mvc:resources автоматически обрабатывать запросы на получение статических данных.


Добавим spring контекст my-context.xml и настройки для spring DispatcherServlet в дескриптор развертывания web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
	http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <servlet>
        <servlet-name>mvc-dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>mvc-dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:/my-context.xml
        </param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>


Добавим в контролер PhonesDirectoryController.js параметр refs и обработчики для компонентов.

Ext.define('PhonesDir.controller.PhonesDirectoryController', {
    extend: 'Ext.app.Controller',

    views: [
        'AddWindowForm',
        'PhonesDirectory',
        'PhonesGrid',
        'SearchPhones'
    ],

    stores: ['PhonesDirectoryStore'],
    models: ['PhonesDirectoryModel'],

    refs: [
        {selector: 'phonesGrid',
            ref: 'phonesGrid'},
        {selector: 'phonesGrid button[action="add"]',
            ref: 'phonesGridAdd'},
        {selector: 'phonesGrid button[action="delete"]',
            ref: 'phonesGridDelete'},
        {selector: 'searchPhones button[action="search"]',
            ref: 'searchPhones'},
        {selector: 'addWindowForm',
            ref: 'addWindowForm'},
        {selector: 'phonesDirectory',
            ref: 'phonesDirectory'},
        {selector: 'addWindowForm textfield[name=name] ',
            ref: 'addWindowFormName'},
        {selector: 'addWindowForm textfield[name=phone]',
            ref: 'addWindowFormPhone'},
        {selector: 'addWindowForm button[action=save]',
            ref: 'addWindowFormPhoneSave'}
    ],

    init: function () {
        this.control({
            'phonesGrid button[action=add]': {
                click: this.onAddPhone
            },
            'phonesGrid button[action=delete]': {
                click: this.onDelPhone
            },
            'searchPhones textfield[name="search"]': {
                change: this.onChangeText
            },
            'phonesGrid': {
                cellclick: this.onLineGrid
            },
            'addWindowForm button[action=save]': {
                click: this.onSavePhone
            },
            'addWindowForm textfield[name=name]': {
                change: this.onValidation
            },
            'addWindowForm textfield[name=phone]': {
                change: this.onValidation
            }
        });
    },

    onSavePhone: function (button) {
        var me = this;
        var phoneModel = Ext.create('PhonesDir.model.PhonesDirectoryModel');
        phoneModel.set(this.getAddWindowForm().down('form').getValues());
        phoneModel.save({
            success: function (operation, response) {
                var objAjax = operation.data;
                me.getPhonesDirectoryStoreStore().add(objAjax);
                me.getAddWindowForm().close();
            },
            failure: function (dummy, result) {
                Ext.MessageBox.show({
                    title: 'Дубликат!',
                    msg: 'Такое имя и телефон уже существуют',
                    buttons: Ext.Msg.OK,
                    icon: Ext.Msg.ERROR
                });
            }

        });
    },

    onAddPhone: function () {
        Ext.widget('addWindowForm');
    },

    onDelPhone: function () {
        var sm = this.getPhonesGrid().getSelectionModel();
        var rs = sm.getSelection();
        this.getPhonesGrid().store.remove(rs[0]);
        this.getPhonesGrid().store.commitChanges();
        this.getPhonesGridDelete().disable();
    },

    onChangeText: function () {
        this.getPhonesDirectoryStoreStore().load({
            params: {
                search: this.getPhonesDirectory().down('searchPhones').getValues()
            }
        });
    },

    onLineGrid: function () {
        this.getPhonesGridDelete().enable();
    },

    onValidation: function () {
        if (this.getAddWindowFormName().validate() && this.getAddWindowFormPhone().validate()) {
            this.getAddWindowFormPhoneSave().enable();
        } else {
            this.getAddWindowFormPhoneSave().disable();
        }
    }

});


  • ref ссылка на что-то в selector'e.
  • selector указывает на компоненты, для быстро обращения к ним через ref.
  • onSavePhone создается модель данных и сохраняется.
  • onAddPhone создает виджет формы добавления.
  • onDelPhone удаляет запись.
  • onChangeText загружает данные в соответствии со значением в поле поиска.
  • onLineGrid при выделении строки кнопка «Удалить» становится активной
  • onValidation валидация полей формы добавления.


И последнее — добавим иконки к кнопкам «Добавить» и «Удалить»:



index.jsp

<%@page contentType="text/html" pageEncoding="utf-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Справочник</title>
    <link rel="stylesheet" type="text/css" href="resources/resources/css/ext-all.css"/>
    <style type="text/css">
        .icon-delete {
            background-image: url(resources/resources/delete.png) !important;
        }

        .icon-add {
            background-image: url(resources/resources/add.png) !important;
        }
    </style>
    <script type="text/javascript" src="resources/ext-all.js"></script>
    <script type="text/javascript" src="app.js"></script>
</head>
<body>
</body>
</html>


Скачать готовый проект.
–5
3246

комментарии (9)

+2
+3 –1
fenst ,  
В следуюшей версии:
— делать пробелы между цифрами (89035050000 -> 8 903 505 0000)
— сделать шрифт и кнопки покрупнее
— в целях оптимизации использования места, выводить данные в 2 столбца (имя, телефон; имя, телефон)
— трансформировать панель поиска из столбца в строку, чтобы она не съедала 1\3 ширину экрана
— добавить категории (отделы, подразделения, города)
— добавить дополнительные поля (городское телефон, второй мобильный телефон)
— добавить подсказку по написнаию имени (Петров Василий или Василий Петров?)
— убрать гигантскую надпись «телефонный справочник», добавить версию программы, добавить контакты разработчика
— добавить статистику: сколько контактов в базе, какая дата её последнего изменения

Но не воспринимайте это как критику, пожалуста. Просто было самому интересно подумать, как можно улучшить ваше творенье.
+2
+3 –1
alterpub ,   * (был изменён)
А затем нужно открыть справочник на мобиле, планшете, нетбуке, ноутбуке с небольшим дисплеем ~13" и разшением 1280х800 и понять что это кровь, кишки, расчлененка =)
+5
script88 ,  
Как мне кажется, без Extjs было бы на много лучше.
+1
jonic ,  
Работал я как то в одной конторке, тоже в качестве клиентской библиотеки использовали Ext.JS. Ну и запарился я с ней, и со вторым новым программистом, который почему то считал своей обязанностью спросить все у меня. Хотя я на день раньше него только узнал что то из Ext.JS. К слову контора эта обанкротилась, не долго просуществовав после моего ухода.

Но из того что знаю, то Ext.JS используется в качестве интерфейса(сильно переработанная) на площадке медицинской по РТ. Но правда у них часто там что то отваливалось. Если тут есть ребята с «барс групп», то может напомнят :)
+5
+6 –1
igordata ,  
Дошкольники благодарны за книжку с картинками!
+1
romario13 ,  
Спасибо автору.

Хороший пост для первого раза. Творческих вам успехов!
+1
tagintsev ,  
Спасибо!
0
+1 –1
Loriowar ,  
Простите, но вид статьи вгоняет в уныние и вызывает панику.
0
getauft ,  
Напомнило лабараторные работы которые нам подсовывли в колледже… Не удивлюсь, если ваша пошаговая инструкция в скором будущем будет распечатана без каких либо изменений и роздана студентам младших курсов… Мне всегда хотелось к таким лабараторным первым пунктом подписать: «Перед началом хардкор копи-паст необходимо выключить мозг»…