Евгений Колесников пре 3 година
родитељ
комит
d6ebf32cef
9 измењених фајлова са 338 додато и 374 уклоњено
  1. 180 110
      articles/android_auth.md
  2. 71 36
      articles/sql_import.md
  3. BIN
      img/dbeaver01.png
  4. BIN
      img/dbeaver02.png
  5. BIN
      img/dbeaver03.png
  6. BIN
      img/dbeaver04.png
  7. BIN
      img/dbeaver05.png
  8. 3 3
      readme.md
  9. 84 225
      shpora/HttpHelper.kt

+ 180 - 110
articles/android_auth.md

@@ -159,7 +159,7 @@
     }
     ```
 
-5. Для запроса данных из базы нужно послать **GET** запрос с названием таблицы. В заголовке запроса передать токен полученный при авторизации (я сначала сделал передачу токена в заголовке, а только потом увидел, что надо было передавать в теле запроса).
+5. Для запроса данных из базы нужно послать **GET** запрос с названием таблицы. В заголовке запроса передать токен полученный при авторизации.
 
     ```
     GET {{url}}/Product
@@ -594,113 +594,169 @@ if(re.containsMatchIn("kolei@ya.ru")){
 
 ## HTTP-запросы, методы, форматы, заголовки.
 
->Класс [**HTTP**](../shpora/HttpHelper.kt) в шпаргалке я обновил.
+В шпаргалке лежит доработанный класс [**HTTP**](../shpora/HttpHelper.kt).
 
-Итак, мы получили от формы авторизации логин и пароль.
+```kt
+object Http {
+    private val client = OkHttpClient()
+
+    fun buildRequest(url: String, data: String? = null, method: String = "GET", headers: Map<String, String>? = null): Request {
+        val json = "application/json; charset=utf-8".toMediaTypeOrNull()
+        val request = Request.Builder().url(url)
+        if (data != null)
+            request.post(data.toRequestBody(json))
+        else
+            request.get()
+
+        if(headers!=null){
+            for((key, value) in headers){
+                request.addHeader(key, value)
+            }
+        }
 
-Напишем нормальную функцию обратного вызова (выделим её в отдельную переменную)
+//            .addHeader("Authorization", "Bearer ${_token}")
+        return request.build()
+    }
 
-Теперь вызов диалога авторизации будет выглядеть так:
+    fun call(url: Any, callback: (response: Response?, error: Exception?)->Unit) {
+        var request: Request = when (url) {
+            is String -> Request.Builder()
+                .url(url)
+                .build()
+            is Request -> url as Request
+            else -> {
+                callback.invoke(null, Exception("Не верный тип параметра \"url\""))
+                return
+            }
+        }
+        client.newCall(request).enqueue(object : Callback {
+            override fun onFailure(call: Call, e: IOException) {
+                callback.invoke(null, Exception(e.message!!))
+            }
+
+            override fun onResponse(call: Call, response: Response) {
+                response.use {
+                    callback.invoke(response, null)
+                }
+            }
+        })
+    }
+}
+```
+
+Разберём что тут понаписано:
 
 ```kt
-LoginDialog(onLoginResponce)
-    .show(supportFragmentManager, null)
+object Http {...}
 ```
 
-Ниже реализация лямбда функции *onLoginResponce*:
+>Ключевое слово **object** одновременно объявляет класс и создаёт его экземпляр (*singleton*).
 
 ```kt
-val onLoginResponce: (login: String, password: String)->Unit = { login, password ->
-    // первым делом сохраняем имя пользователя, 
-    // чтобы при необходимости можно было разлогиниться
-    username = login
-
-    // затем формируем JSON объект с нужными полями
-    val json = JSONObject()
-    json.put("username", login)
-    json.put("password", password)
-
-    // и вызываем POST-запрос /login
-    // в параметрах не забываем указать заголовок Content-Type
-    HTTP.requestPOST(
-        "http://s4a.kolei.ru/login",
-        json,
-        mapOf(
-            "Content-Type" to "application/json"
-        )
-    ){result, error ->
-        if(result!=null){
-            try {
-                // анализируем ответ
-                val jsonResp = JSONObject(result)
-
-                // если нет объекта notice
-                if(!jsonResp.has("notice"))
-                    throw Exception("Не верный формат ответа, ожидался объект notice")
-
-                // есть какая-то ошибка
-                if(jsonResp.getJSONObject("notice").has("answer"))
-                    throw Exception(jsonResp.getJSONObject("notice").getString("answer"))
-
-                // есть токен!!!
-                if(jsonResp.getJSONObject("notice").has("token")) {
-                    token = jsonResp.getJSONObject("notice").getString("token")
-                    runOnUiThread {
-                        // тут можно переходить на следующее окно
-                        Toast.makeText(this, "Success get token: $token", Toast.LENGTH_LONG)
-                            .show()
-                    }
-                }
-                else
-                    throw Exception("Не верный формат ответа, ожидался объект token")
-            } catch (e: Exception){
-                runOnUiThread {
-                    AlertDialog.Builder(this)
-                        .setTitle("Ошибка")
-                        .setMessage(e.message)
-                        .setPositiveButton("OK", null)
-                        .create()
-                        .show()
-                }
-            }
-        } else
-            runOnUiThread {
-                AlertDialog.Builder(this)
-                    .setTitle("Ошибка http-запроса")
-                    .setMessage(error)
-                    .setPositiveButton("OK", null)
-                    .create()
-                    .show()
-            }
+fun call(
+    url: Any, 
+    callback: (response: Response?, error: Exception?)->Unit) 
+{
+    var request: Request = when (url) {
+        is String -> Request.Builder()
+            .url(url)
+            .build()
+        is Request -> url as Request
+        else -> {
+            callback.invoke(
+                null, 
+                Exception("Не верный тип параметра \"url\""))
+            return
+        }
     }
+    ...
+```
+
+**call** - основной метод этого класса, он, собственно, и запускает запрос. Я сделал его универсальным: на входе ему можно передать либо просто строку URL, либо подготовленный объект **Request** (он нам понадобится, чтобы посылать запросы с методом POST, данными и заголовками)
+
+```kt
+fun buildRequest(url: String, data: String? = null, headers: Map<String, String>? = null): Request {
+    val json = "application/json; charset=utf-8".toMediaTypeOrNull()
+    val request = Request.Builder().url(url)
+    if (data != null)
+        request.post(data.toRequestBody(json))
+    else
+        request.get()
+
+    if(headers!=null){
+        for((key, value) in headers){
+            request.addHeader(key, value)
+        }
+    }
+    return request.build()
 }
 ```
 
-Если мы уже авторизованы и получили в ответ соответсвующую ошибку, то нужно предусмотреть на форме кнопку "Выход" (Logout) и реализовать обработчик:
+**buildRequest** - вспомогательный метод, который облегчает построение объекта Request.
+
+>Этот метод заточен на посылку GET или POST запросов с типом данных JSON (заголовок `Content-Type: application/json` добавляется автоматически). Если понадобится послать другой метод (PUT, PATCH, DELETE) или данные другого типа, то можно сформировать объект **Request** самим и передать его в метод **call**
+
+Дальше по ходу лекции мы разберёмся с различными примерами использования этого метода (но они есть и в комментариях в начале файла)
+
+Итак, мы получили от формы авторизации логин и пароль.
+
+По клику на кнопке "Авторизоваться" пытаемся получить токен авторизации
 
 ```kt
-// тут я не делал отдельный json объект для параметров
-// можно создавать его на ходу
-HTTP.requestPOST(
-    "http://s4a.kolei.ru/logout",
-    JSONObject().put("username", username),
-    mapOf(
-        "Content-Type" to "application/json"
+// первым делом сохраняем имя пользователя, 
+// чтобы при необходимости можно было разлогиниться
+username = login
+
+// затем формируем JSON объект с нужными полями
+val json = JSONObject()
+json.put("username", login)
+json.put("password", password)
+
+// и вызываем POST-запрос /login
+Http.call(
+    Http.buildRequest(
+        "http://s4a.kolei.ru/login",
+        json.toString()
     )
-){result, error ->
-    // при выходе не забываем стереть существующий токен
-    token = ""
-
-    // каких-то осмысленных действий дальше не предполагается
-    // разве что снова вызвать форму авторизации
-    runOnUiThread {
-        if(result!=null) {
-            Toast.makeText(this, "Logout success!", Toast.LENGTH_LONG).show()
+) { response, error ->
+    try {
+        // если в запросе получено исключение, то "выбрасываем" его
+        if (error != null) throw error
+
+        // если ответ получен, но код не 200, то тоже "выбрасываем" исключение
+        if (!response!!.isSuccessful) throw Exception(response.message)
+
+
+        // анализируем ответ
+        val jsonResp = JSONObject(response.body!!.string())
+
+        // если нет объекта notice
+        if(!jsonResp.has("notice"))
+            throw Exception("Не верный формат ответа, ожидался объект notice")
+
+        // есть какая-то ошибка
+        if(jsonResp.getJSONObject("notice").has("answer"))
+            throw Exception(jsonResp.getJSONObject("notice").getString("answer"))
+
+        // есть токен!!!
+        if(jsonResp.getJSONObject("notice").has("token")) {
+            // сохраняем токен в свойстве класса
+            token = jsonResp.getJSONObject("notice").getString("token")
+
+            runOnUiThread {
+                // тут можно переходить на следующее окно
+                Toast.makeText(this, "Success get token: $token", Toast.LENGTH_LONG)
+                    .show()
+            }
         }
-        else {
+        else
+            throw Exception("Не верный формат ответа, ожидался объект token")
+    } catch (e: Exception) {
+        // любую ошибку показываем на экране
+        runOnUiThread {
             AlertDialog.Builder(this)
-                .setTitle("Ошибка http-запроса")
-                .setMessage(error)
+                .setTitle("Ошибка")
+                .setMessage(e.message)
                 .setPositiveButton("OK", null)
                 .create()
                 .show()
@@ -709,31 +765,43 @@ HTTP.requestPOST(
 }
 ```
 
+Если мы уже авторизованы и получили в ответ соответсвующую ошибку, то нужно предусмотреть на форме кнопку "Выход" (Logout) и реализовать обработчик (для краткости я убрал проверку результата в начале и обработку исключения в конце - добавьте сами):
+
+```kt
+// тут для примера я формирую JSON-строку 
+// без использования класса JSONObject
+Http.call(Http.buildRequest(
+    "http://s4a.kolei.ru/logout", 
+    """{"username":"${userName}"}"""))
+{response, error ->
+    try {
+        ...
+
+        // при выходе не забываем стереть существующий токен
+        token = ""
+
+        // каких-то осмысленных действий дальше не предполагается
+        // разве что снова вызвать форму авторизации
+        runOnUiThread {
+            Toast.makeText(this, "Logout success!", Toast.LENGTH_LONG).show()
+        }
+
+        ...
+```
+
 Ну и последний на сегодня запрос - запрос списка продукции (его нужно вызывать уже из другого **activity**, передав ему токен):
 
 ```kt
-if(token.isNotEmpty()){
-    HTTP.requestGET(
+Http.call(
+    Http.buildRequest(
         "http://s4a.kolei.ru/Product",
-        mapOf(
-            "token" to token
-        )
-    ){result, error ->
-        runOnUiThread{
-            if(result!=null){
-                resultTextView.text = result
-            }
-            else
-                resultTextView.text = "ошибка: $error"
-        }
-    }
-}
-else
-    Toast.makeText(this, "Не найден токен, нужно залогиниться", Toast.LENGTH_LONG)
-        .show()
+        headers = mapOf("token" to token)
+    )
+) { response, error ->
+    ...
 ```
 
-Как видите, в параметры GET-запроса я добавил заголовки, чтобы можно было добавить токен. Дальнейшая реализация уже с вас.
+Как видите, в параметры метода **buildRequest** я добавил заголовки, чтобы можно было добавить токен. Дальнейшая реализация уже с вас.
 
 
 ## Сохранение данных при работе приложения
@@ -759,7 +827,7 @@ else
     }
     ```
 
-2. В манифесте в тег **application** добавьте атрибут *android:name=".MyApp"*, где *.MyApp* это имя созданного нами ранее класса
+2. В манифесте в тег **application** добавьте атрибут `android:name=".MyApp"`, где `.MyApp` это имя созданного нами ранее класса
 
 3. В классах, где нам нужны глобальные переменные создаем переменную, которая будет хранить указатель на **MyApp**
 
@@ -851,6 +919,8 @@ productTypeSpinner.adapter = ArrayAdapter(
 
 И при выборе элемента списка сделать фильтрацию списка продукции (тоже сделайте сами)
 
+<!-- TODO вроде нигде не расписано как делать фильтрацию - написать -->
+
 ```kt
 productTypeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
     override fun onItemSelected(

+ 71 - 36
articles/sql_import.md

@@ -6,12 +6,16 @@
 <a href="../articles/cs_mysql_connection2.md">Создание подключения к БД MySQL. Получение данных с сервера.
 </a></td><tr></table>
 
-# Создание базы данных. 
+# Восстановление базы данных из скрипта. 
 
-## Подключение к базе данных
+>Мы получаем скрипты для генерации структуры БД либо в задании на демо-экзамене, либо формируем из ER-диаграммы. Создание самой БД мы не рассматриваем (ни на демо-экзамене, ни на моем сервере у вас нет на это прав), но там ничего сложного и при желании вы можете установить локальную БД дома и с ней делать что угодно.
+
+## Подключение к базе данных (MySQL)
 
 Прежде чем что-то сделать с базой данных нужно к ней подключиться:
 
+### Используя MySQL Workbench
+
 1. Запустите программу *MySQL Workbench*
 
 2. На стартовом окне программы кликните по значку "+"
@@ -30,9 +34,11 @@
 
         ![](../img/01024.png)
 
+        Если при попытке подключения ругается на сертификат, то перейдите в закладку **Advanced** и в блоке **Others** напишите строку `useSSL=0`
+
     * В окне настроек соединения жмите "OK"
 
-## Создание БД используя скрипт
+#### Формирование DDL-скрипта по диаграмме
 
 1. Окройте EER-диаграмму
 2. Переименуйте (правый клик мышкой) базу данных в соответствии со своим *Username* (см. выше)
@@ -57,7 +63,33 @@
     ![](../img/01029.png)
 
 
->На демо-экзамене в ресурсах будет аналогичный файл скрипта базы данных, но в нём не укзана база данных. Нужно в начале скрипта вписать команду `use <название вашей базы>`
+>На демо-экзамене в ресурсах будет аналогичный файл скрипта базы данных, но в нём не укзана база данных. Нужно в начале скрипта вписать команду `use <название вашей базы>;`
+
+### Используя dBeaver
+
+**dBeaver** - кроссплатформенный бесплатный менеджер баз данных (список поддерживаемых БД большой, есть и MySQL и MSSQL)
+
+#### Создание подключения
+
+В списке проектов открываем пункт **General** и в контекстном меню пункта **Connections** выбираем **Создать -> Соединение**
+
+![Создание подключения](../img/dbeaver01.png)
+
+В открывшемся окне выберите тип соедиения (используемую БД)
+
+У меня на сервере установлена `mysql  Ver 14.14 Distrib 5.7.39, for Linux (x86_64)`, поэтому выбираем MySQL. Если у вас другой тип БД, то выбираете соответсвующий.
+
+![Выбор типа БД](../img/dbeaver02.png)
+
+В настройках соединения указываем доменное имя или IP сервера, название базы данных и имя пользователя (выдаст преподаватель или сами знаете какие в вашей БД), пароль.
+
+Затем нажимаете **тест соединения** и, если всё введено правильно, и соединение устанавливается то жмёте **Готово**
+
+![Настройки соединения](../img/dbeaver03.png)
+
+В списке соединений появится созданое соединение, название состоит из имени базы и имени сервера и имеющиеся базы данных.
+
+![Список соединений](../img/dbeaver05.png)
 
 # Импорт данных.
 
@@ -175,7 +207,7 @@ CSV расшифровывается как comma-separated values — «зна
 
 2. Открываем или импортируем файл с исходными данными
 
-    >Файл материалов у нас в формате *TXT*. Такой формат напрямую из Excel открыть нельзя - загружайте через *импорт данных* (в разных версиях Excel меню может называться по0разному)
+    >Файл материалов у нас в формате *TXT*. Такой формат напрямую из Excel открыть нельзя - загружайте через *импорт данных* (в разных версиях Excel меню может называться по-разному)
     >
     >![](../img/01034.png)
 
@@ -217,8 +249,6 @@ CSV расшифровывается как comma-separated values — «зна
 
 4. Экспорт данных
 
-    >**ВАЖНО!!!** Нигде не написано, но названия полей при импорте должны быть латиницей, поэтому переименовываем заголовки столбов.
-
     MySQL может импортировать данные только в *CSV*-формате. Файл либо "сохраняем как", либо "экспортируем"
 
     ![](../img/01041.png)
@@ -319,13 +349,13 @@ CSV расшифровывается как comma-separated values — «зна
 
         ```sql
         INSERT INTO `ekolesnikov`.`MaterialType`
-        (`ID`,
-        `Title`,
-        `DefectedPercent`)
+            (`ID`,
+            `Title`,
+            `DefectedPercent`)
         VALUES
-        (<{ID: }>,
-        <{Title: }>,
-        <{DefectedPercent: }>);
+            (<{ID: }>,
+            <{Title: }>,
+            <{DefectedPercent: }>);
         ```
 
         т.е. у нас уже указана база и таблица, и перечислены все поля этой таблицы.
@@ -340,28 +370,31 @@ CSV расшифровывается как comma-separated values — «зна
 
         ```sql
         INSERT INTO `ekolesnikov`.`MaterialType`
-        (`Title`,
-        `DefectedPercent`)
-        SELECT `material_import`.`name`,
+            (`Title`,
+            `DefectedPercent`)
+        SELECT 
+            `material_import`.`name`,
             `material_import`.`type`,
             `material_import`.`count`,
             `material_import`.`edizm`,
             `material_import`.`quantity`,
             `material_import`.`min_ost`,
             `material_import`.`price`
-        FROM `ekolesnikov`.`material_import`;
+        FROM 
+            `ekolesnikov`.`material_import`;
         ```
 
     * Количество и порядок выбираемых полей должны соответствовать вставляемым полям, перечисленным в круглых скобках запроса **INSERT**. Редактируем **SELECT** запрос:
 
         ```sql
         INSERT INTO `ekolesnikov`.`MaterialType`
-        (`Title`,
-        `DefectedPercent`)
+            (`Title`,
+            `DefectedPercent`)
         SELECT 
             DISTINCT `material_import`.`type`, 
             0
-        FROM `ekolesnikov`.`material_import`;
+        FROM 
+            `ekolesnikov`.`material_import`;
         ```
 
         т.е. мы убрали все поля, кроме *type* (тип материала), поставили перед ним ключевое слово **DISTINCT** (выбирать только уникальные) и для поля *DefectedPercent* записали константу "0"
@@ -382,34 +415,36 @@ CSV расшифровывается как comma-separated values — «зна
 
         ```sql
         INSERT INTO `ekolesnikov`.`Material`
-        (`Title`,
-        `CountInPack`,
-        `Unit`,
-        `CountInStock`,
-        `MinCount`,
-        `Cost`,
-        `MaterialTypeID`)
+            (`Title`,
+            `CountInPack`,
+            `Unit`,
+            `CountInStock`,
+            `MinCount`,
+            `Cost`,
+            `MaterialTypeID`)
         ```
 
     * Вставляем "Select All Statement" из таблицы **material_import**    
 
         ```sql
         INSERT INTO `ekolesnikov`.`Material`
-        (`Title`,
-        `CountInPack`,
-        `Unit`,
-        `CountInStock`,
-        `MinCount`,
-        `Cost`,
-        `MaterialTypeID`)
-        SELECT `material_import`.`name`,
+            (`Title`,
+            `CountInPack`,
+            `Unit`,
+            `CountInStock`,
+            `MinCount`,
+            `Cost`,
+            `MaterialTypeID`)
+        SELECT 
+            `material_import`.`name`,
             `material_import`.`type`,
             `material_import`.`count`,
             `material_import`.`edizm`,
             `material_import`.`quantity`,
             `material_import`.`min_ost`,
             `material_import`.`price`
-        FROM `ekolesnikov`.`material_import`;
+        FROM 
+            `ekolesnikov`.`material_import`;
         ```
 
         Вспоминаем, что порядок и количество полей должны совпадать и редактируем запрос:

BIN
img/dbeaver01.png


BIN
img/dbeaver02.png


BIN
img/dbeaver03.png


BIN
img/dbeaver04.png


BIN
img/dbeaver05.png


+ 3 - 3
readme.md

@@ -236,7 +236,7 @@ https://office-menu.ru/uroki-sql Уроки SQL
 
 <!-- переделать на asp.net -->
 
-## Тема 5.1.6. C#, разбор задания.
+## Тема 5.1.6. C#, разбор заданий предыдущих лет.
 
 <!-- 1. [Сессия 1. Создание БД. Импорт данных. Окно авторизации](./articles/wsr_1.md) -->
 1. [HTTP запросы в C#. Получение списка материалов выбранного продукта](./articles/cs_http.md)
@@ -252,8 +252,8 @@ https://office-menu.ru/uroki-sql Уроки SQL
 + HTTP-запросы, разбор JSON, XML
 + Разработка АПИ (php-сервер)
 + Мобильное приложение
-ER-диаграмма
-Проектирование (Use-case, UML)
++ ER-диаграмма
++ Проектирование (Use-case, UML)
 Формирование отчетов (PDF)
 Формирование графиков
 + Тестирование

+ 84 - 225
shpora/HttpHelper.kt

@@ -1,257 +1,116 @@
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import org.json.JSONObject
-import java.io.*
-import java.net.HttpURLConnection
-import java.net.URL
-import java.net.URLEncoder
-import javax.net.ssl.HttpsURLConnection
+// тут не забыть установить свой пакет
+package ru.yotc.baza
 
-/*
-Перед использованием не забудьте добавить в манифест
+import okhttp3.*
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.RequestBody.Companion.toRequestBody
+import okio.IOException
 
-разрешение
+/*
+В манифест добавьте разрешение на работу с интернетом
 <uses-permission android:name="android.permission.INTERNET" />
 
-И атрибут в тег application
, если на сайте нет сертификата, атрибут в тег **application**:
 android:usesCleartextTraffic="true"
 
-Использование:
-
-HTTP.requestGET(
-    "http://s4a.kolei.ru/Product",
-    mapOf(
-        "token" to token
-    )
-){result, error, code ->
-    runOnUiThread{
-        if(result!=null){
-            resultTextView.text = result
-        }
-        else
-            AlertDialog.Builder(this)
-                .setTitle("Ошибка http-запроса")
-                .setMessage(error)
-                .setPositiveButton("OK", null)
-                .create()
-                .show()
-    }
-}
+В зависимости проекта добавить билиотеку:
+implementation 'com.squareup.okhttp3:okhttp:4.10.0'
+*/
 
-HTTP.requestPOST(
-    "http://s4a.kolei.ru/login",
-    JSONObject().put("username", username).put("password", password),
-    mapOf(
-        "Content-Type" to "application/json"
-    )
-){result, error, code ->
-    runOnUiThread{
-        if(result!=null){
-        }
-        else
+/*
+Использование для GET-запросов:
+Http.call("урл строка"){ response, error ->
+    try {
+        // если в запросе получено исключение, то "выбрасываем" его
+        if (error != null) throw error
+
+        // если ответ получен, но код не 200, то тоже "выбрасываем" исключение
+        if (!response!!.isSuccessful) throw Exception(response.message)
+
+        // тут обработка результата:
+        // тело ответа как строка: response.body!!.string()
+        // тело ответа как изображение: BitmapFactory.decodeStream(response.body!!.byteStream())
+
+    } catch (e: Exception) {
+        // любую ошибку показываем на экране
+        runOnUiThread {
             AlertDialog.Builder(this)
-                .setTitle("Ошибка http-запроса")
-                .setMessage(error)
+                .setTitle("Ошибка")
+                .setMessage(e.message)
                 .setPositiveButton("OK", null)
                 .create()
                 .show()
-    }
-}
-
-HTTP.getImage("https://openweathermap.org/img/w/${icoName}.png") { bitmap, error ->
-    runOnUiThread {
-        if (bitmap != null) {
-            var imageView = findViewById<ImageView>(R.id.ico)
-            imageView.setImageBitmap(bitmap)
+            }
         }
     }
 }
 */
 
-object HTTP
-{
-    private const val GET : String = "GET"
-    private const val POST : String = "POST"
-
-    private fun getCharSet(url: String): String{
-        val obj = URL(url)
-        var con: HttpURLConnection = if(url.startsWith("https:", true))
-            obj.openConnection() as HttpsURLConnection
-        else
-            obj.openConnection() as HttpURLConnection
-
-        con.requestMethod = "HEAD"
-        val contentType = con.contentType
-        return if(contentType!=null && contentType.contains("Windows-1251", true))
-            "Windows-1251"
-        else
-            "UTF-8"
-    }
+/*
+Использование для POST-запросов, или для запросов с заголовками
 
-    /**
-     * Метод для отправки POST-запросов
-     *
-     * Запросы отправляются в отдельном потоке
-     * Автоматически поддерживает http/httpS
-     * Можно задать заголовки запроса
-     * По-умолчанию отправляет данные в формате application/x-www-form-urlencoded
-     * при задании заголовка Content-type: application/json автоматически переключается на это тип
-     *
-     * @param url Полный URL сайта (протокол + домен + путь)
-     * @param postData Даные для отправки
-     * @param headers Ассоциативный массив заголовков запроса
-     * @param callback Лямбда-функция обратного вызова
-     */
-    fun requestPOST(
-        url: String,
-        postData: JSONObject? = null,
-        headers: Map<String, String>?,
-        callback: (result: String?, error: String, code: Int)->Unit
-    ) {
-        Thread( Runnable {
-            var error = ""
-            var result: String? = null
-            var code: Int = -1
-            try {
-                val charset = getCharSet(url)
+val json = JSONObject()
+json.put("username", userName)
+json.put("password", password)
 
-                val urlURL = URL(url)
-                val conn: HttpURLConnection = if (url.startsWith("https:", true))
-                    urlURL.openConnection() as HttpsURLConnection
-                else
-                    urlURL.openConnection() as HttpURLConnection
+Http.call(
+    Http.buildRequest(
+        "http://s4a.kolei.ru/login",
+        json.toString()
+    )
+) { response, error -> ... }
+*/
 
-                // если задан тип контента application/json, то на выход пишу как есть
-                var contentTypeJson = false
-                if(headers!=null){
-                    for((key, value) in headers){
-                        if(key.lowercase()=="content-type" && value.startsWith("application/json"))
-                            contentTypeJson = true
-                        conn.setRequestProperty(key, value)
-                    }
-                }
+/*
+Использование для запросов с заголовками
+Http.call(
+    Http.buildRequest(
+        "http://s4a.kolei.ru/Product",
+        headers = mapOf("token" to token)
+    )
+) { response, error -> ... }
+*/
 
-                conn.readTimeout = 10000
-                conn.connectTimeout = 10000
-                conn.requestMethod = POST
-                conn.doInput = true
-                conn.doOutput = true
-                val os: OutputStream = conn.outputStream
+object Http {
+    private val client = OkHttpClient()
 
-                if (postData != null) {
-                    val writer = BufferedWriter(OutputStreamWriter(os, "UTF-8"))
-                    var content = ""
-                    content = if(contentTypeJson)
-                        postData.toString()
-                    else
-                        encodeParams(postData)?:""
-                    writer.write(content)
-                    writer.flush()
-                    writer.close()
-                }
+    fun buildRequest(url: String, data: String? = null, method: String = "GET", headers: Map<String, String>? = null): Request {
+        val json = "application/json; charset=utf-8".toMediaTypeOrNull()
+        val request = Request.Builder().url(url)
+        if (data != null)
+            request.post(data.toRequestBody(json))
+        else
+            request.get()
 
-                os.close()
-                code = conn.responseCode // To Check for 200
-                if (code == HttpsURLConnection.HTTP_OK) {
-                    val `in` = BufferedReader(InputStreamReader(conn.inputStream, charset))
-                    val sb = StringBuffer("")
-                    var line: String? = ""
-                    while (`in`.readLine().also { line = it } != null) {
-                        sb.append(line)
-                        break
-                    }
-                    `in`.close()
-                    result = sb.toString()
-                }
-                else {
-                    error = "Response code ${code}"
-                }
-            }
-            catch (e: Exception) {
-                error = e.message.toString()
+        if(headers!=null){
+            for((key, value) in headers){
+                request.addHeader(key, value)
             }
-            callback.invoke(result, error, code)
-        }).start()
+        }
+        return request.build()
     }
 
-    fun getImage(url: String, callback: (result: Bitmap?, error: String)->Unit){
-        Thread( Runnable {
-            var image: Bitmap? = null
-            var error = ""
-            try {
-                val `in` = URL(url).openStream()
-                image = BitmapFactory.decodeStream(`in`)
+    fun call(url: Any, callback: (response: Response?, error: Exception?)->Unit) {
+        var request: Request = when (url) {
+            is String -> Request.Builder()
+                .url(url)
+                .build()
+            is Request -> url as Request
+            else -> {
+                callback.invoke(null, Exception("Не верный тип параметра \"url\""))
+                return
             }
-            catch (e: Exception) {
-                error = e.message.toString()
+        }
+        client.newCall(request).enqueue(object : Callback {
+            override fun onFailure(call: Call, e: IOException) {
+                callback.invoke(null, Exception(e.message!!))
             }
-            callback.invoke(image, error)
-        }).start()
-    }
-
-    fun requestGET(
-        r_url: String,
-        headers: Map<String, String>?,
-        callback: (result: String?, error: String, code: Int)->Unit
-    ) {
-        Thread( Runnable {
-            var error = ""
-            var result: String? = null
-            var code: Int = -1
-            try {
-                val charset = getCharSet(r_url)
-
-                val obj = URL(r_url)
 
-                val con: HttpURLConnection = if(r_url.startsWith("https:", true))
-                    obj.openConnection() as HttpsURLConnection
-                else
-                    obj.openConnection() as HttpURLConnection
-
-                if(headers!=null){
-                    for((key, value) in headers){
-                        con.setRequestProperty(key, value)
-                    }
+            override fun onResponse(call: Call, response: Response) {
+                response.use {
+                    callback.invoke(response, null)
                 }
-
-                con.requestMethod = GET
-                code = con.responseCode
-
-                result = if (code == HttpURLConnection.HTTP_OK) { // connection ok
-                    val `in` =
-                        BufferedReader(InputStreamReader(con.inputStream, charset))
-                    var inputLine: String?
-                    val response = StringBuffer()
-                    while (`in`.readLine().also { inputLine = it } != null) {
-                        response.append(inputLine)
-                    }
-                    `in`.close()
-                    response.toString()
-                } else {
-                    null
-                }
-            }
-            catch (e: Exception){
-                error = e.message.toString()
             }
-
-            callback.invoke(result, error, code)
-        }).start()
-    }
-
-    @Throws(IOException::class)
-    private fun encodeParams(params: JSONObject): String? {
-        val result = StringBuilder()
-        var first = true
-        val itr = params.keys()
-        while (itr.hasNext()) {
-            val key = itr.next()
-            val value = params[key]
-            if (first) first = false else result.append("&")
-            result.append(URLEncoder.encode(key, "UTF-8"))
-            result.append("=")
-            result.append(URLEncoder.encode(value.toString(), "UTF-8"))
-        }
-        return result.toString()
+        })
     }
 }