📜 ⬆️ ⬇️

Web application development

Hi, Habr!

In the study of technology Ext JS and Java, wrote a web-application "Car Catalog". I want to share this experience with you.

Appearance and functionality of the application





Addition and Validation



Editing

Search

')

Instruments


  1. IntelliJ IDEA 13 download
  2. Ext JS 5.0.1 download
  3. Apache Tomcat 8.0.12 download
  4. MySQL 5.6.20 download
  5. Apache Maven 3.0.5 download
  6. Java 1.8.0_20 download
  7. Java doc read
  8. Ext JS Guides to read
  9. Hibernate read
  10. JPA read
  11. Spring read
  12. SQL read
  13. MVC read
  14. DAO read
  15. Layer Service read
  16. Deployment descriptor read


Project creation

Specify the Java path in the Project SDK:


Enter the path to your Maven home directory:


"Maven projects need to be imported" click on Enable Auto-Import


Add server
Add Tomcat Server:




In the Application server, specify the path to the Tomcat server:



Ok -> Apply -> Ok

Check that everything works:





Client (ExtJS)


Add files to the Ext JS framework:


Ext JS MVC model:


Create the app.js file:


app.js
Ext.application({ name: 'CarCatalog', launch: function () { Ext.create('Ext.container.Viewport', { layout: 'fit', items: { xtype: 'panel', html: '<h2> </h2>' } }); } }); 


In the index.jsp file, we include the Ext JS styles, then the Ext JS framework, and only then 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/packages/ext-theme-neptune/build/resources/ext-theme-neptune-all.css"/> <script type="text/javascript" src="resources/ext-all.js"></script> <script type="text/javascript" src="resources/packages/ext-theme-neptune/build/ext-theme-neptune.js"></script> <script type="text/javascript" src="app.js"></script> </head> <body> </body> </html> 


Check that everything works:


View

Four types will be needed - this is the SearchCarView.js search type , the CarGridView.js table view , the AddCarFormView.js data addition form view, and the CarCatalogView.js framework view , where all the views will be placed .


SerachCarView.js
 Ext.define('CarCatalog.view.SearchCarView', { extend: 'Ext.form.Panel', alias: 'widget.searchCarView', bodyPadding: 10, items: [ { xtype: 'textfield', name: 'search', fieldLabel: '  ', maxLength: 200 } ] }); 


CarGridView.js
 Ext.define('CarCatalog.view.CarGridView', { extend: 'Ext.grid.Panel', alias: 'widget.carGridView', width: 400, height: 300, frame: true, iconCls: 'icon-user', viewConfig:{ markDirty:false }, columns: [ { text: '', flex: 1, sortable: true, dataIndex: 'name', editor: { xtype:'textfield', allowBlank: false, blankText: '    ' } }, { flex: 2, text: '', sortable: true, dataIndex: 'price', editor: { xtype:'textfield', regex: /^([0-9]{1,20})*$/, regexText: '    ', allowBlank: false, blankText: '    ' } } ], plugins: [ Ext.create('Ext.grid.plugin.RowEditing', { clicksToEdit: 2, saveBtnText: '', cancelBtnText: '' }) ], selType: 'rowmodel', dockedItems: [ { xtype: 'toolbar', items: [ { text: '', action: 'add', iconCls: 'icon-add' }, '-', { action: 'delete', text: '', iconCls: 'icon-delete', disabled: true } ] } ] }); 


AddCarFormView.js
 Ext.define('CarCatalog.view.AddCarFormView', { extend: 'Ext.window.Window', alias: 'widget.addCarFormView', autoShow: true, layout: 'fit', modal: true, items: [ { bodyPadding: 10, xtype: 'form', items: [ { xtype: 'textfield', name: 'name', fieldLabel: ' ', allowBlank: false, blankText: '    ' }, { xtype: 'textfield', name: 'price', fieldLabel: '', regex: /^([0-9]{1,20})*$/, regexText: '    ', allowBlank: false, blankText: '    ' } ] } ], buttons: [ { text: '', action: 'save', disabled: true }, { text: '', handler: function () { this.up('window').close(); } } ] }); 


CarCatalogView.js
 Ext.define('CarCatalog.view.CarCatalogView', { extend: 'Ext.panel.Panel', width: 500, height: 360, padding: 10, alias: 'widget.carCatalogView', layout: 'border', items: [ { xtype: 'carGridView', region: 'center' }, { xtype: 'panel', html: '<div style="font: normal 18px cursive"><center><font size = "10"> </font></center></div>', region: 'north', height: 80 }, { xtype: 'searchCarView', title: '', region: 'west', width: 300, collapsible: true, collapsed: false } ], renderTo: Ext.getBody() }); 


Let's point out the types in app.js :
 Ext.application({ name: 'CarCatalog', views: [ 'AddCarFormView', 'CarCatalogView', 'CarGridView', 'SearchCarView' ], launch: function () { Ext.create('Ext.container.Viewport', { layout: 'fit', items: { xtype : 'carCatalogView' } }); } }); 


Check that everything works:


Controller




CarCatalogController.js
 Ext.define('CarCatalog.controller.CarCatalogController', { extend: 'Ext.app.Controller', init: function () { this.control({ }); } }); 



Model and storage



CarCatalogModel.js
 Ext.define('CarCatalog.model.CarCatalogModel', { extend: 'Ext.data.Model', fields: ['name', 'price'], proxy: { type: 'rest', api: { create: 'car', read: 'car', destroy: 'car', update: 'car' }, reader: { type: 'json', root: 'data', successProperty: 'success' }, writer: { type: 'json', writeAllFields: true } } }); 


CarCatalogStore.js
 Ext.define('CarCatalog.store.CarCatalogStore', { extend: 'Ext.data.Store', requires : [ 'CarCatalog.model.CarCatalogModel' ], model: 'CarCatalog.model.CarCatalogModel', autoLoad: true, autoSync: true, proxy: { type: 'rest', api: { create: 'car', read: 'car', destroy: 'car', update: 'car' }, reader: { type: 'json', root: 'data', successProperty: 'success' }, writer: { type: 'json', writeAllFields: true } } }); 


Add the Store: 'CarCatalogStore' parameter to CarGridView.js to display the data in the table:


Let us indicate CarCatalogController.js controller and CarCatalogStore.js storage in app.js :
 Ext.application({ name: 'CarCatalog', views: [ 'AddCarFormView', 'CarCatalogView', 'CarGridView', 'SearchCarView' ], controllers : [ 'CarCatalogController' ], stores : [ 'CarCatalogStore' ], launch: function () { Ext.create('Ext.container.Viewport', { layout: 'fit', items: { xtype : 'carCatalogView' } }); } }); 


Check that everything works. 404 (Not Found) - this is normal, as there is still nothing at localhost : 8080 / car:


Server (Java)


Add dependencies to 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> 


Create a java folder:


Create a data model and data access layer (DAO):


Car.java
 @Entity @Table(name = "cars") public class Car implements Serializable { @Id @Column(name = "ID") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name") private String name; @Column(name = "price") private Long price; public Car() { } 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 Long getPrice() { return price; } public void setPrice(Long price) { this.price = price; } } 


CarDao.java
 public interface CarDao { void add(Car car); void update(Car car); void delete(Car car); Collection<Car> getCars(String search); public List findByCar(String name, Long price); } 


CarDaoImpl.java
 public class CarDaoImpl implements CarDao { @PersistenceContext private EntityManager emf; @Override public void add(Car car) { emf.persist(car); } @Override public void update(Car car) { emf.merge(car); } @Override public void delete(Car car) { emf.remove(emf.getReference(Car.class, car.getId())); } @Override public Collection<Car> getCars(String search) { if (null == search || search.trim().isEmpty()) { return emf.createQuery( "select c from Car c") .getResultList(); } return emf.createQuery( "select c from Car c where c.name like :search") .setParameter("search", search.trim() + "%") .getResultList(); } public List<Car> findByCar(String name, Long price) { return emf.createQuery( "select c from Car c where c.name = :name and c.price = :price") .setParameter("name", name) .setParameter("price", price) .getResultList(); } } 


Create a service layer:


CarService.java
 public interface CarService { Boolean add(Car car); void update(Car car); Collection<Car> getCars(String search); void delete(Car car); } 


CarServiceImpl.java
 public class CarServiceImpl implements CarService { private CarDao carDao; public CarDao getCarDao() { return carDao; } public void setCarDao(CarDao carDao) { this.carDao = carDao; } @Transactional @Override public Boolean add(Car car) { List<Car> duplicate = carDao.findByCar(car.getName(), car.getPrice()); if (duplicate.isEmpty()) { carDao.add(car); return true; } return false; } @Transactional @Override public void update(Car car) { carDao.update(car); } @Transactional @Override public Collection<Car> getCars(String search) { return carDao.getCars(search); } @Transactional @Override public void delete(Car car) { carDao.delete(car); } } 


Let's create a controller that will be mapped to the / car address to process requests from the client:


CarController.java
 @Controller @RequestMapping("/car") public class CarController { @Autowired private CarService carService; @RequestMapping(method = RequestMethod.GET) @ResponseBody public Collection<Car> getCars(String search) { return carService.getCars(search); } @RequestMapping(method = RequestMethod.POST) @ResponseBody public ExtResult setCar(@RequestBody Car car) { return new ExtResult(carService.add(car), car); } @RequestMapping(value = "{id}", method = RequestMethod.DELETE) @ResponseBody public String deleteCar(@RequestBody Car car) { carService.delete(car); return "delete"; } @RequestMapping(value = "{id}", method = RequestMethod.PUT) @ResponseBody public String updateCar(@RequestBody Car car) { carService.update(car); return "update"; } } 




ExtResult.java
 public class ExtResult { private boolean success; private Car data; public ExtResult(boolean success, Car data) { this.success = success; this.data = data; } public ExtResult() { } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public Car getData() { return data; } public void setData(Car data) { this.data = data; } } 


Check that everything works. Build the project with maven install and run the application.

Create spring context my-context.xml c:



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/CarCatalog?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="carDao" class="model.dao.impl.CarDaoImpl"> </bean> <bean name="CarService" class="service.impl.CarServiceImpl"> <property name="carDao" ref="carDao"> </property> </bean> </beans> 


Create a database called CarCatalog and utf_general_ci coding or change the name in the context of my-context.xml to your own. Also change p: username and p: password if it does not match.

Create settings for spring DispatcherServlet, which will process requests from the client:


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> 


Add spring context my-context.xml and settings for spring DispatcherServlet to web.xml deployment descriptor:
 <?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> 


Add the refs parameter and component handlers to the CarCatalogController.js controller:
 Ext.define('CarCatalog.controller.CarCatalogController', { extend: 'Ext.app.Controller', refs: [ {selector: 'carGridView', ref: 'carGridView'}, {selector: 'carGridView button[action="add"]', ref: 'carGridAdd'}, {selector: 'carGridView button[action="delete"]', ref: 'carGridDelete'}, {selector: 'searchCarView button[action="search"]', ref: 'searchCar'}, {selector: 'addCarFormView', ref: 'addCarFormView'}, {selector: 'carCatalogView', ref: 'carCatalogView'}, {selector: 'addCarFormView textfield[name=name] ', ref: 'addCarFormName'}, {selector: 'addCarFormView textfield[name=price]', ref: 'addCarFormPrice'}, {selector: 'addCarFormView button[action=save]', ref: 'addCarFormSave'} ], init: function () { this.control({ 'carGridView button[action=add]': { click: this.onAddCar }, 'carGridView button[action=delete]': { click: this.onDelCar }, 'searchCarView textfield[name="search"]': { change: this.onChangeText }, 'carGridView': { cellclick: this.onLineGrid }, 'addCarFormView button[action=save]': { click: this.onSaveCar }, 'addCarFormView textfield[name=name]': { change: this.onValidation }, 'addCarFormView textfield[name=price]': { change: this.onValidation } }); }, onSaveCar: function (button) { var me = this; var carModel = Ext.create('CarCatalog.model.CarCatalogModel'); carModel.set(this.getAddCarFormView().down('form').getValues()); carModel.save({ success: function (operation, response) { var objAjax = operation.data; Ext.getStore('CarCatalogStore').add(objAjax); me.getAddCarFormView().close(); }, failure: function (dummy, result) { Ext.MessageBox.show({ title: '!', msg: '     ', buttons: Ext.Msg.OK, icon: Ext.Msg.ERROR }); } }); }, onAddCar: function () { Ext.widget('addCarFormView'); }, onDelCar: function () { var sm = this.getCarGridView().getSelectionModel(); var rs = sm.getSelection(); this.getCarGridView().store.remove(rs[0]); this.getCarGridView().store.commitChanges(); this.getCarGridDelete().disable(); }, onChangeText: function () { Ext.getStore('CarCatalogStore').load({ params: { search: this.getCarCatalogView().down('searchCarView').getValues() } }); }, onLineGrid: function () { this.getCarGridDelete().enable(); }, onValidation: function () { if (this.getAddCarFormName().validate() && this.getAddCarFormPrice().validate()) { this.getAddCarFormSave().enable(); } else { this.getAddCarFormSave().disable(); } } }); 


And the last thing - let's add icons to the “Add” and “Delete” buttons.


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/packages/ext-theme-neptune/build/resources/ext-theme-neptune-all.css"/> <script type="text/javascript" src="resources/ext-all.js"></script> <script type="text/javascript" src="resources/packages/ext-theme-neptune/build/ext-theme-neptune.js"></script> <script type="text/javascript" src="app.js"></script> <style type="text/css"> .icon-delete { background-image: url(resources/delete.png) !important; } .icon-add { background-image: url(resources/add.png) !important; } </style> </head> <body> </body> </html> 


Source: https://habr.com/ru/post/240033/


All Articles