From 8e96ec67206c616af5f2d4d1284f31b7e227d51b Mon Sep 17 00:00:00 2001 From: hmalik144 Date: Thu, 5 Mar 2020 17:15:57 +0000 Subject: [PATCH] Initial commit --- .gitignore | 2 + .idea/.gitignore | 2 + .idea/compiler.xml | 13 ++ .idea/misc.xml | 14 ++ JobApplications.iml | 2 + pom.xml | 136 +++++++++++++++++ src/main/java/JobObject.kt | 16 ++ src/main/java/LegacyCode.kt | 142 ++++++++++++++++++ src/main/java/Main.kt | 99 ++++++++++++ src/main/java/api/network/BasicInterceptor.kt | 23 +++ src/main/java/api/network/NetworkRequests.kt | 31 ++++ src/main/java/api/network/ReedApi.kt | 41 +++++ src/main/java/api/network/SafeApiRequest.kt | 35 +++++ .../api/network/responses/ReedJobObject.kt | 17 +++ .../api/network/responses/ReedResponse.kt | 8 + 15 files changed, 581 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/misc.xml create mode 100644 JobApplications.iml create mode 100644 pom.xml create mode 100644 src/main/java/JobObject.kt create mode 100644 src/main/java/LegacyCode.kt create mode 100644 src/main/java/Main.kt create mode 100644 src/main/java/api/network/BasicInterceptor.kt create mode 100644 src/main/java/api/network/NetworkRequests.kt create mode 100644 src/main/java/api/network/ReedApi.kt create mode 100644 src/main/java/api/network/SafeApiRequest.kt create mode 100644 src/main/java/api/network/responses/ReedJobObject.kt create mode 100644 src/main/java/api/network/responses/ReedResponse.kt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..744289d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Project exclude paths +/target/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..5c98b42 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..aebeede --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d24ea8e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/JobApplications.iml b/JobApplications.iml new file mode 100644 index 0000000..78b2cc5 --- /dev/null +++ b/JobApplications.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0dba17b --- /dev/null +++ b/pom.xml @@ -0,0 +1,136 @@ + + + 4.0.0 + + org.example + JobApplications + 1.0-SNAPSHOT + + + 1.3.61 + 1.7 + 1.7 + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + + + org.apache.poi + poi + 4.1.1 + + + org.apache.poi + poi-ooxml + 4.1.1 + + + + org.seleniumhq.selenium + selenium-chrome-driver + 3.141.59 + + + org.seleniumhq.selenium + selenium-java + 3.141.59 + + + com.tylerthrailkill.helpers + pretty-print + 2.0.2 + + + + com.squareup.retrofit2 + retrofit + 2.6.0 + + + com.squareup.retrofit2 + converter-gson + 2.6.0 + + + + org.json + json + 20190722 + + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + 1.3.3 + + + + + + ${project.basedir}/src/main/java + ${project.basedir}/src/test/java + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + 1.8 + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.6 + + + make-assembly + package + single + + + + Main + + + + jar-with-dependencies + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/JobObject.kt b/src/main/java/JobObject.kt new file mode 100644 index 0000000..43eeaf5 --- /dev/null +++ b/src/main/java/JobObject.kt @@ -0,0 +1,16 @@ +import java.util.* + +data class JobObject( + var jobId: String? = null, + var website: String? = null, + var jobTitle: String? = null, + var location: String? = null, + var company: String? = null, + var url: String? = null, + var dateApplied : String? = null +){ + + override fun toString(): String { + return super.toString() + } +} \ No newline at end of file diff --git a/src/main/java/LegacyCode.kt b/src/main/java/LegacyCode.kt new file mode 100644 index 0000000..f6d1be4 --- /dev/null +++ b/src/main/java/LegacyCode.kt @@ -0,0 +1,142 @@ +import Constants.Companion.REED_KEYWORDS +import Constants.Companion.REED_LOCATION +import Constants.Companion.REED_PASSWORD +import Constants.Companion.REED_USERNAME +import org.openqa.selenium.By +import org.openqa.selenium.WebElement +import org.openqa.selenium.chrome.ChromeDriver +import org.openqa.selenium.support.ui.ExpectedConditions +import org.openqa.selenium.support.ui.WebDriverWait +import kotlin.math.roundToInt + +fun StartSearch(){ + + //Open Chrome + System.setProperty("webdriver.chrome.driver","C:\\Selenium\\selenium-java-3.141.59\\chromedriver_win32\\chromedriver.exe" ) + val driver = ChromeDriver() + + //open reed website login + driver.get("https://www.reed.co.uk/account/signin?returnUrl=%2F#&card=signin") + val wait = WebDriverWait(driver, 20) + + //wait for page to load + val lastElementToLoad = driver.findElementById("signin-button") + wait.until(ExpectedConditions.elementToBeClickable(lastElementToLoad)) + + //insert credentials and sign in + driver.findElementByXPath("//*[@id=\"Credentials_Email\"]").sendKeys(REED_USERNAME) + driver.findElementByXPath("//*[@id=\"Credentials_Password\"]").sendKeys(REED_PASSWORD) + lastElementToLoad.click() + + //wait for page to load + val jobSearchEditText = driver.findElementByXPath("//*[@id=\"keywords\"]") + wait.until(ExpectedConditions.elementToBeClickable(jobSearchEditText)) + +// //submit search + jobSearchEditText.sendKeys(REED_KEYWORDS) + driver.findElementByXPath("//*[@id=\"location\"]").sendKeys(REED_LOCATION) + driver.findElementByXPath("//*[@id=\"main-search\"]/div[1]/div[3]/button").click() + + //todo: change to wait + Thread.sleep(1500) + + val ad = driver.findElementByXPath("//*[@id=\"content\"]/div[1]/div[2]/h1") + wait.until(ExpectedConditions.elementToBeClickable(ad)) + + //find number of pages + val text = driver.findElementByCssSelector("div.page-counter").text /* eg. 1 - 25 of 99 jobs */ + print(text) + val count = text.toTotalCount() + val pages = count.getNumberOfPages() + + //loop through pages of search + for (i in 1..pages){ + + //open page by number on search + //todo: change this url builder + driver.get("https://www.reed.co.uk/jobs/android-developer-jobs-in-kilburn-london?pageno=$i") + Thread.sleep(2500) + + //elements list of jobs on page + val list = driver.findElementsByCssSelector("div.col-sm-12.col-md-9.col-lg-10.details") + + //turn list into global list job object + list.forEach { + val badge = it.findElement(By.cssSelector("div.badge-container")) + //check if there is a badge element + if (badge.isDisplayed){ + //see if applied is in badge + val applied = badge.findElements(By.cssSelector("span.label.label-applied")) + //if applied doesnt exist then add to global list of jobs + if (applied.isNullOrEmpty()){ + val jobObject = it.toJobObject() + jobsList.add(jobObject) + } + + }else{ + //no badge exists so add to list of jobs declared at the top + val jobObject = it.toJobObject() + jobsList.add(jobObject) + } + } + } + + //loop through the jobs collected + jobsList.forEach{ + //open the URl + driver.get(it.url) + + val title = driver.findElementByXPath("//*[@id=\"content\"]/div/div[2]/article") + wait.until(ExpectedConditions.elementToBeClickable(title)) + + //check for external apply element + val applyExternal = driver.findElementsByCssSelector("span.external-app-caption") + + //if external apply is empty then apply for job + if (applyExternal.isNullOrEmpty()){ + + print(it.jobTitle + " ${it.url} \n" ) + + + //find apply button + val applyNow = driver.findElementsByXPath("//*[@id=\"applyButtonSide\"]") + + if (!applyNow.isNullOrEmpty()){ + + //click apply + applyNow[1].click() + + try{ + val successfulApplied = driver.findElementByXPath("//*[@id=\"content\"]/div/div[1]/a") + wait.until(ExpectedConditions.visibilityOf(successfulApplied)) + }catch (e: Exception){ + println(it.jobId + " did not apply") + println("\n" + e.toString() + "\n") + } + } + } + } + +} + +fun String.toTotalCount() : Int = this.substringAfter("of ").substringBefore( " jobs").toInt() + +fun Int.getNumberOfPages():Int = if (this % 25 ==0){ + this/25; +}else{ + (this.toDouble()/25).roundToInt() +} + +fun WebElement.toJobObject():JobObject { + val attribute = this.findElement(By.tagName("a")) + + val id = attribute.getAttribute("data-id") + val url = attribute.getAttribute("href") + val jobtitle = attribute.getAttribute("title") + + val location = this.findElement(By.xpath("//*[@id=\"jobSection${id}\"]/div[1]/div[1]/ul[2]/li")).text + val company = this.findElement(By.xpath("//*[@id=\"jobSection${id}\"]/div[1]/header/div[2]/a")).text + + return JobObject("reed-${id}","Reed.co.uk",jobtitle,location,company,url) + +} \ No newline at end of file diff --git a/src/main/java/Main.kt b/src/main/java/Main.kt new file mode 100644 index 0000000..cf8aa40 --- /dev/null +++ b/src/main/java/Main.kt @@ -0,0 +1,99 @@ +import Constants.Companion.REED_PASSWORD +import Constants.Companion.REED_USERNAME +import api.network.NetworkRequests +import api.network.responses.ReedJobObject +import org.openqa.selenium.JavascriptExecutor +import org.openqa.selenium.chrome.ChromeDriver +import org.openqa.selenium.support.ui.ExpectedConditions +import org.openqa.selenium.support.ui.WebDriverWait + + +var jobsList = mutableListOf() +var reedJobsList = listOf() +lateinit var driver: ChromeDriver +lateinit var wait: WebDriverWait + +public fun main(args: Array){ + setup() + setupWebDriver() + logonToReed() + applyForJobsThroughLoop() +} + +fun setup(){ + val result = NetworkRequests().getSearchApi() + + if (!result.isNullOrEmpty()){ + reedJobsList = result + } + +} + +fun setupWebDriver(){ + //Open Chrome + System.setProperty("webdriver.chrome.driver","C:\\Selenium\\selenium-java-3.141.59\\chromedriver_win32\\chromedriver.exe" ) + driver = ChromeDriver() + wait = WebDriverWait(driver, 20) +} + +fun applyForJobsThroughLoop(){ + + reedJobsList.forEach { + try { + driver.get(it.jobUrl) + applyForJob(it) + + }catch (e: Exception){ + println("\n" + e.toString() + "\n") + } + } +} + +fun logonToReed(){ + driver.get("https://www.reed.co.uk/account/signin?returnUrl=%2F#&card=signin") + + //wait for page to load + val lastElementToLoad = driver.findElementById("signin-button") + wait.until(ExpectedConditions.elementToBeClickable(lastElementToLoad)) + + //insert credentials and sign in + driver.findElementByXPath("//*[@id=\"Credentials_Email\"]").sendKeys(REED_USERNAME) + driver.findElementByXPath("//*[@id=\"Credentials_Password\"]").sendKeys(REED_PASSWORD) + lastElementToLoad.click() + + //wait for page to load + val jobSearchEditText = driver.findElementByXPath("//*[@id=\"keywords\"]") + wait.until(ExpectedConditions.elementToBeClickable(jobSearchEditText)) +} + +fun applyForJob(jobObject: ReedJobObject){ + val appliedBefore = driver.findElementsByXPath("//*[@id=\"content\"]/div/div[2]/article/div/div[1]/div") + + if (appliedBefore.isNullOrEmpty()){ + println("${jobObject.jobId} has not been applied") + //find apply button + val applyNow = driver.findElementsByXPath("//*[@id=\"applyButtonSide\"]") + + if (!applyNow.isNullOrEmpty()){ + //click apply + val index = if (applyNow.size > 1){ 1 }else{ 0 } + applyNow[index].click() + + try{ +// val successfulApplied = driver.findElementByCssSelector("div.alert.alert-success alert-borderless") + wait.until{ + driver.executeScript("return document.readyState") == "complete" + } + + }catch (e: Exception){ + println("\n" + e.toString() + "\n") + } + } + }else{ + println("${jobObject.jobId} has been applied") + } + + + +} + diff --git a/src/main/java/api/network/BasicInterceptor.kt b/src/main/java/api/network/BasicInterceptor.kt new file mode 100644 index 0000000..712d1fb --- /dev/null +++ b/src/main/java/api/network/BasicInterceptor.kt @@ -0,0 +1,23 @@ +package api.network + +import Constants.Companion.REED_API_KEY +import okhttp3.Credentials +import okhttp3.Interceptor +import okhttp3.Response + +class BasicInterceptor() : Interceptor{ + + private val credentials = Credentials.basic(REED_API_KEY,"") + + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request(); + val builder = request.newBuilder().header( + "Authorization", credentials + ).build() + + return chain.proceed(builder) + } + + +} \ No newline at end of file diff --git a/src/main/java/api/network/NetworkRequests.kt b/src/main/java/api/network/NetworkRequests.kt new file mode 100644 index 0000000..ce3c3ae --- /dev/null +++ b/src/main/java/api/network/NetworkRequests.kt @@ -0,0 +1,31 @@ +package api.network + +import Constants.Companion.REED_KEYWORDS +import Constants.Companion.REED_LOCATION +import Constants.Companion.REED_MINIMUM_SALARY +import api.network.responses.ReedJobObject +import com.example.h_mal.androiddevelopertechtest_incrowdsports.data.network.ReedApi +import com.example.h_mal.androiddevelopertechtest_incrowdsports.data.network.SafeApiRequest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +class NetworkRequests : SafeApiRequest(){ + + val reedApi = ReedApi() + + fun getSearchApi() : List?{ + try { + val response = runBlocking { apiRequest { reedApi.getGameData(REED_KEYWORDS, REED_LOCATION, REED_MINIMUM_SALARY) } } + + response.results?.let { + return it + } + + }catch (e : Exception){ + println("*** $e") + } + return null + } +} \ No newline at end of file diff --git a/src/main/java/api/network/ReedApi.kt b/src/main/java/api/network/ReedApi.kt new file mode 100644 index 0000000..763dd31 --- /dev/null +++ b/src/main/java/api/network/ReedApi.kt @@ -0,0 +1,41 @@ +package com.example.h_mal.androiddevelopertechtest_incrowdsports.data.network + +import ReedResponse +import api.network.BasicInterceptor +import api.network.responses.ReedJobObject +import okhttp3.OkHttpClient +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + + +interface ReedApi { + + //get the game data from api call of relevent gameID + @GET("search") + suspend fun getGameData(@Query("keywords") keywords: String, + @Query("locationName") locationName: String, + @Query("minimumSalary") minimumSalary: String) : Response + + //instantiate api class + companion object{ + operator fun invoke() : ReedApi{ + val okkHttpclient = OkHttpClient.Builder() + .addNetworkInterceptor(BasicInterceptor()) + .build() + + //return api class ss retrofit client + return Retrofit.Builder() + .client(okkHttpclient) + .baseUrl("https://www.reed.co.uk/api/1.0/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(ReedApi::class.java) + } + } + +} + diff --git a/src/main/java/api/network/SafeApiRequest.kt b/src/main/java/api/network/SafeApiRequest.kt new file mode 100644 index 0000000..3556e49 --- /dev/null +++ b/src/main/java/api/network/SafeApiRequest.kt @@ -0,0 +1,35 @@ +package com.example.h_mal.androiddevelopertechtest_incrowdsports.data.network + +import org.json.JSONException +import org.json.JSONObject +import retrofit2.Response +import java.io.IOException + +abstract class SafeApiRequest { + + //abstract function to unwrap body from response of api call + suspend fun apiRequest(call: suspend () -> Response) : T{ + //get the reponse + val response = call.invoke() + if(response.isSuccessful){ + //response is successful so return the body + return response.body()!! + }else{ + //the response failed so throw an error + + val error = response.errorBody()?.string() + + val message = StringBuilder() + error?.let{ + try{ + message.append(JSONObject(it).getString("message")) + }catch(e: JSONException){ } + message.append("\n") + } + message.append("Error Code: ${response.code()}") + + throw IOException(message.toString()) + } + } + +} \ No newline at end of file diff --git a/src/main/java/api/network/responses/ReedJobObject.kt b/src/main/java/api/network/responses/ReedJobObject.kt new file mode 100644 index 0000000..c2d496c --- /dev/null +++ b/src/main/java/api/network/responses/ReedJobObject.kt @@ -0,0 +1,17 @@ +package api.network.responses +data class ReedJobObject( + val date: String?, + val employerProfileId: String?, + val locationName: String?, + val maximumSalary: String?, + val jobTitle: String?, + val employerName: String?, + val employerProfileName: String?, + val jobId: Int?, + val employerId: String?, + val minimumSalary: String?, + val jobUrl: String?, + val jobDescription: String?, + val expirationDate: String?, + val applications: Int? +) \ No newline at end of file diff --git a/src/main/java/api/network/responses/ReedResponse.kt b/src/main/java/api/network/responses/ReedResponse.kt new file mode 100644 index 0000000..399e118 --- /dev/null +++ b/src/main/java/api/network/responses/ReedResponse.kt @@ -0,0 +1,8 @@ + + +import api.network.responses.ReedJobObject + +data class ReedResponse ( + val results : List?, + val totalResults : Int? +) \ No newline at end of file