diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/Address.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/Address.kt new file mode 100644 index 00000000000..9993f5451af --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/Address.kt @@ -0,0 +1,3 @@ +package ru.practicum.android.diploma.data.components + +data class Address(val raw: String?) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/Area.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/Area.kt new file mode 100644 index 00000000000..933efcd0ef0 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/Area.kt @@ -0,0 +1,6 @@ +package ru.practicum.android.diploma.data.components + +data class Area( + val id: Int, + val name: String +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/Contacts.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/Contacts.kt new file mode 100644 index 00000000000..5c71b573fd6 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/Contacts.kt @@ -0,0 +1,7 @@ +package ru.practicum.android.diploma.data.components + +data class Contacts( + val email: String, + val name: String, + val phones: List?, +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/Employer.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/Employer.kt new file mode 100644 index 00000000000..564c1e18fe3 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/Employer.kt @@ -0,0 +1,8 @@ +package ru.practicum.android.diploma.data.components + +import com.google.gson.annotations.SerializedName + +data class Employer( + val name: String, + @SerializedName("logo_urls") val logoUrls: LogoUrls? +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/Employment.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/Employment.kt new file mode 100644 index 00000000000..a353db1692b --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/Employment.kt @@ -0,0 +1,6 @@ +package ru.practicum.android.diploma.data.components + +data class Employment( + val id: String, + val name: String +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/Experience.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/Experience.kt new file mode 100644 index 00000000000..88dfb6d93d3 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/Experience.kt @@ -0,0 +1,6 @@ +package ru.practicum.android.diploma.data.components + +data class Experience( + val id: String, + val name: String +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/KeySkill.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/KeySkill.kt new file mode 100644 index 00000000000..d7d2a5af0b9 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/KeySkill.kt @@ -0,0 +1,5 @@ +package ru.practicum.android.diploma.data.components + +data class KeySkill( + val name: String +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/LogoUrls.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/LogoUrls.kt new file mode 100644 index 00000000000..30c307b2d16 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/LogoUrls.kt @@ -0,0 +1,7 @@ +package ru.practicum.android.diploma.data.components + +import com.google.gson.annotations.SerializedName + +data class LogoUrls( + @SerializedName("240") val logo240: String? +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/Phone.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/Phone.kt new file mode 100644 index 00000000000..4bf6ced6b3c --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/Phone.kt @@ -0,0 +1,8 @@ +package ru.practicum.android.diploma.data.components + +data class Phone( + val city: String, + val comment: String?, + val country: String, + val formatted: String, +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/Salary.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/Salary.kt new file mode 100644 index 00000000000..8530de235f5 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/Salary.kt @@ -0,0 +1,8 @@ +package ru.practicum.android.diploma.data.components + +data class Salary( + val currency: String?, + val from: Int?, + val to: Int?, + val gross: Boolean? +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/components/Schedule.kt b/app/src/main/java/ru/practicum/android/diploma/data/components/Schedule.kt new file mode 100644 index 00000000000..8a1732c05d4 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/components/Schedule.kt @@ -0,0 +1,6 @@ +package ru.practicum.android.diploma.data.components + +data class Schedule( + val id: String, + val name: String +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/Converters.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/Converters.kt new file mode 100644 index 00000000000..4a61b59e00c --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/Converters.kt @@ -0,0 +1,16 @@ +package ru.practicum.android.diploma.data.dto + +import ru.practicum.android.diploma.domain.models.Vacancy + +fun VacancyDto.toVacancy(): Vacancy { + return Vacancy( + id = id, + name = name, + company = employer.name, + currency = salary?.currency.orEmpty(), + salaryFrom = salary?.from, + salaryTo = salary?.to, + area = area.name, + icon = employer.logoUrls?.logo240 ?: "" + ) +} diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/Response.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/Response.kt index 45dd655c968..f0189cfa984 100644 --- a/app/src/main/java/ru/practicum/android/diploma/data/dto/Response.kt +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/Response.kt @@ -5,6 +5,6 @@ const val RESULT_CODE_SUCCESS = 200 const val RESULT_CODE_BAD_REQUEST = 400 const val RESULT_CODE_SERVER_ERROR = 500 -class Response { +open class Response { var resultCode = 0 } diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/SearchResponse.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/SearchResponse.kt new file mode 100644 index 00000000000..6dc5548f00b --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/SearchResponse.kt @@ -0,0 +1,8 @@ +package ru.practicum.android.diploma.data.dto + +data class SearchResponse( + val items: List, + val found: Int, + val pages: Int, + val page: Int, +) : Response() diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyDto.kt new file mode 100644 index 00000000000..baa54a203b4 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyDto.kt @@ -0,0 +1,13 @@ +package ru.practicum.android.diploma.data.dto + +import ru.practicum.android.diploma.data.components.Area +import ru.practicum.android.diploma.data.components.Employer +import ru.practicum.android.diploma.data.components.Salary + +data class VacancyDto( + val id: Int, + val name: String, + val employer: Employer, + val salary: Salary?, + val area: Area +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyResponse.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyResponse.kt new file mode 100644 index 00000000000..03ab61f0755 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/VacancyResponse.kt @@ -0,0 +1,30 @@ +package ru.practicum.android.diploma.data.dto + +import com.google.gson.annotations.SerializedName +import ru.practicum.android.diploma.data.components.Address +import ru.practicum.android.diploma.data.components.Area +import ru.practicum.android.diploma.data.components.Contacts +import ru.practicum.android.diploma.data.components.Employer +import ru.practicum.android.diploma.data.components.Employment +import ru.practicum.android.diploma.data.components.Experience +import ru.practicum.android.diploma.data.components.KeySkill +import ru.practicum.android.diploma.data.components.Salary +import ru.practicum.android.diploma.data.components.Schedule + +data class VacancyResponse( + val id: Int, + val name: String, + val employer: Employer, + val salary: Salary?, + val area: Area, + @SerializedName("alternate_url") + val alternateUrl: String, + val description: String, + val employment: Employment?, + val experience: Experience?, + val schedule: Schedule?, + val contacts: Contacts?, + @SerializedName("key_skills") + val keySkills: List, + val address: Address? +) : Response() diff --git a/app/src/main/java/ru/practicum/android/diploma/data/impl/SearchRepositoryImpl.kt b/app/src/main/java/ru/practicum/android/diploma/data/impl/SearchRepositoryImpl.kt new file mode 100644 index 00000000000..d290cc0b1a7 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/impl/SearchRepositoryImpl.kt @@ -0,0 +1,58 @@ +package ru.practicum.android.diploma.data.impl + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import ru.practicum.android.diploma.data.dto.RESULT_CODE_BAD_REQUEST +import ru.practicum.android.diploma.data.dto.RESULT_CODE_NO_INTERNET +import ru.practicum.android.diploma.data.dto.Response +import ru.practicum.android.diploma.data.dto.SearchRequest +import ru.practicum.android.diploma.data.dto.SearchResponse +import ru.practicum.android.diploma.data.dto.VacancyDto +import ru.practicum.android.diploma.data.dto.toVacancy +import ru.practicum.android.diploma.data.network.NetworkClient +import ru.practicum.android.diploma.domain.api.SearchRepository +import ru.practicum.android.diploma.domain.models.VacanciesResponse +import ru.practicum.android.diploma.util.Options +import ru.practicum.android.diploma.util.ResponseData + +class SearchRepositoryImpl(private val networkClient: NetworkClient) : SearchRepository { + + override fun search(options: Options): Flow> = flow { + emit( + when (val response = networkClient.doRequest(SearchRequest(Options.toMap(options)))) { + is SearchResponse -> { + with(response) { + val vacanciesResponse = VacanciesResponse( + items.map(VacancyDto::toVacancy), + found, + page, + pages, + ) + ResponseData.Data(vacanciesResponse) + } + } + + else -> { + responseToError(response) + } + } + ) + } + + private fun responseToError(response: Response): ResponseData = + ResponseData.Error( + when (response.resultCode) { + RESULT_CODE_NO_INTERNET -> { + ResponseData.ResponseError.NO_INTERNET + } + + RESULT_CODE_BAD_REQUEST -> { + ResponseData.ResponseError.CLIENT_ERROR + } + + else -> { + ResponseData.ResponseError.SERVER_ERROR + } + } + ) +} diff --git a/app/src/main/java/ru/practicum/android/diploma/data/interceptors/HeaderInterceptor.kt b/app/src/main/java/ru/practicum/android/diploma/data/interceptors/HeaderInterceptor.kt new file mode 100644 index 00000000000..3b04951c418 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/interceptors/HeaderInterceptor.kt @@ -0,0 +1,24 @@ +package ru.practicum.android.diploma.data.interceptors + +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Response +import ru.practicum.android.diploma.BuildConfig + +object HeaderInterceptor : Interceptor { + private const val USER_AGENT_AUTHORIZATION = "Authorization: Bearer ${BuildConfig.HH_ACCESS_TOKEN}" + private const val USER_AGENT_APP_NAME = "HH-User-Agent: CareerHub (e.gasymov@hh.ru)" + + private val headers = Headers.Builder() + .add(USER_AGENT_AUTHORIZATION) + .add(USER_AGENT_APP_NAME) + .build() + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request().newBuilder() + .headers(headers) + .build() + + return chain.proceed(request) + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/data/interceptors/LoggingInterceptor.kt b/app/src/main/java/ru/practicum/android/diploma/data/interceptors/LoggingInterceptor.kt new file mode 100644 index 00000000000..1f13e4da805 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/interceptors/LoggingInterceptor.kt @@ -0,0 +1,48 @@ +package ru.practicum.android.diploma.data.interceptors + +import android.util.Log +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import okio.Buffer +import java.nio.charset.Charset +import kotlin.math.min + +object LoggingInterceptor : Interceptor { + private val CHARSET: Charset = Charset.forName("UTF-8") + private const val MAX_LOGCAT_SIZE = 4_000L + private const val LOGCAT_TAG = "LoggingInterceptor" + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + logRequest(request) + + val response = chain.proceed(request) + logResponse(response) + return response + } + + private fun logRequest(request: Request) { + logD("request =>\n${request.url()}") + val buffer = Buffer() + request.body()?.writeTo(buffer) + while (!buffer.exhausted()) { + logD(buffer.readString(min(MAX_LOGCAT_SIZE, buffer.size()), CHARSET)) + } + } + + private fun logResponse(response: Response) { + response.body()?.apply { + source().request(Long.MAX_VALUE) + val buffer = source().buffer.clone() + logD("response =>\n") + while (!buffer.exhausted()) { + logD(buffer.readString(min(MAX_LOGCAT_SIZE, buffer.size()), CHARSET)) + } + } + } + + private fun logD(message: String) { + Log.d(LOGCAT_TAG, message) + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/HHApiService.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/HHApiService.kt index 539330c5837..da49e2cebfb 100644 --- a/app/src/main/java/ru/practicum/android/diploma/data/network/HHApiService.kt +++ b/app/src/main/java/ru/practicum/android/diploma/data/network/HHApiService.kt @@ -1,21 +1,15 @@ package ru.practicum.android.diploma.data.network import retrofit2.http.GET -import retrofit2.http.Headers import retrofit2.http.Path import retrofit2.http.QueryMap -import ru.practicum.android.diploma.BuildConfig -import ru.practicum.android.diploma.data.dto.Response - -const val USER_AGENT_AUTHORIZATION = "Authorization: Bearer ${BuildConfig.HH_ACCESS_TOKEN}" -const val USER_AGENT_APP_NAME = "HH-User-Agent: CareerHub (e.gasymov@hh.ru)" +import ru.practicum.android.diploma.data.dto.SearchResponse +import ru.practicum.android.diploma.data.dto.VacancyResponse interface HHApiService { - @Headers(USER_AGENT_AUTHORIZATION, USER_AGENT_APP_NAME) @GET("vacancies/{vacancy_id}") - suspend fun getVacancy(@Path("vacancy_id") id: Int): Response + suspend fun getVacancy(@Path("vacancy_id") id: Int): VacancyResponse - @Headers(USER_AGENT_AUTHORIZATION, USER_AGENT_APP_NAME) @GET("vacancies") - suspend fun searchVacancies(@QueryMap options: Map): Response + suspend fun searchVacancies(@QueryMap options: Map): SearchResponse } diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt index c9d3a6f6897..3ddccf97473 100644 --- a/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt +++ b/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt @@ -22,10 +22,10 @@ class RetrofitNetworkClient( if (!isInternetAvailable(context)) { return Response().apply { resultCode = RESULT_CODE_NO_INTERNET } } - return getResponce(dto = dto) + return getResponse(dto = dto) } - private suspend fun getResponce(dto: Any): Response { + private suspend fun getResponse(dto: Any): Response { return withContext(Dispatchers.IO) { try { when (dto) { diff --git a/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt b/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt index eff76dc5014..0f373bd95a1 100644 --- a/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt +++ b/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt @@ -1,11 +1,14 @@ package ru.practicum.android.diploma.di import androidx.room.Room +import okhttp3.OkHttpClient import org.koin.android.ext.koin.androidContext import org.koin.dsl.module import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import ru.practicum.android.diploma.data.db.AppDatabase +import ru.practicum.android.diploma.data.interceptors.HeaderInterceptor +import ru.practicum.android.diploma.data.interceptors.LoggingInterceptor import ru.practicum.android.diploma.data.network.HHApiService import ru.practicum.android.diploma.data.network.NetworkClient import ru.practicum.android.diploma.data.network.RetrofitNetworkClient @@ -20,8 +23,14 @@ val dataModule = module { } single { + val client = OkHttpClient.Builder() + .addInterceptor(LoggingInterceptor) + .addInterceptor(HeaderInterceptor) + .build() + Retrofit.Builder() .baseUrl(BASE_URL) + .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() .create(HHApiService::class.java) diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/api/SearchRepository.kt b/app/src/main/java/ru/practicum/android/diploma/domain/api/SearchRepository.kt new file mode 100644 index 00000000000..27acaf74b29 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/api/SearchRepository.kt @@ -0,0 +1,10 @@ +package ru.practicum.android.diploma.domain.api + +import kotlinx.coroutines.flow.Flow +import ru.practicum.android.diploma.domain.models.VacanciesResponse +import ru.practicum.android.diploma.util.Options +import ru.practicum.android.diploma.util.ResponseData + +interface SearchRepository { + fun search(options: Options): Flow> +} diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/models/VacanciesResponse.kt b/app/src/main/java/ru/practicum/android/diploma/domain/models/VacanciesResponse.kt new file mode 100644 index 00000000000..e8641fe27a2 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/models/VacanciesResponse.kt @@ -0,0 +1,8 @@ +package ru.practicum.android.diploma.domain.models + +data class VacanciesResponse( + val results: List, + val foundVacancies: Int, + val page: Int, + val pages: Int, +) diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/models/Vacancy.kt b/app/src/main/java/ru/practicum/android/diploma/domain/models/Vacancy.kt new file mode 100644 index 00000000000..98f81c28f0e --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/models/Vacancy.kt @@ -0,0 +1,12 @@ +package ru.practicum.android.diploma.domain.models + +data class Vacancy( + val id: Int, + val name: String, + val company: String, + val currency: String, + val salaryFrom: Int?, + val salaryTo: Int?, + val area: String, + val icon: String +) diff --git a/app/src/main/java/ru/practicum/android/diploma/util/Options.kt b/app/src/main/java/ru/practicum/android/diploma/util/Options.kt new file mode 100644 index 00000000000..e9c760ca56a --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/Options.kt @@ -0,0 +1,17 @@ +package ru.practicum.android.diploma.util + +data class Options( + val searchText: String, + val itemsPerPage: Int, + val page: Int, +) { + companion object { + fun toMap(options: Options): Map = with(options) { + mapOf( + "text" to searchText, + "per_page" to itemsPerPage.toString(), + "page" to page.toString(), + ) + } + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/util/ResponseData.kt b/app/src/main/java/ru/practicum/android/diploma/util/ResponseData.kt new file mode 100644 index 00000000000..e6de210db3d --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/ResponseData.kt @@ -0,0 +1,13 @@ +package ru.practicum.android.diploma.util + +sealed interface ResponseData { + data class Data(val value: T) : ResponseData + data class Error(val error: ResponseError) : ResponseData + + enum class ResponseError { + NO_INTERNET, + CLIENT_ERROR, + SERVER_ERROR, + NOT_FOUND + } +}