Initial commit

This commit is contained in:
2020-03-05 17:15:57 +00:00
commit 8e96ec6720
15 changed files with 581 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Project exclude paths
/target/

2
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
# Default ignored files
/workspace.xml

13
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="JobApplications" />
</profile>
</annotationProcessing>
</component>
</project>

14
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

2
JobApplications.iml Normal file
View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4" />

136
pom.xml Normal file
View File

@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>JobApplications</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<kotlin.version>1.3.61</kotlin.version>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-chrome-driver</artifactId>
<version>3.141.59</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
<dependency>
<groupId>com.tylerthrailkill.helpers</groupId>
<artifactId>pretty-print</artifactId>
<version>2.0.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.retrofit/retrofit -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<version>2.6.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.json/json -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20190722</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.3.3</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals> <goal>single</goal> </goals>
<configuration>
<archive>
<manifest>
<mainClass>Main</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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()
}
}

142
src/main/java/LegacyCode.kt Normal file
View File

@@ -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)
}

99
src/main/java/Main.kt Normal file
View File

@@ -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<JobObject>()
var reedJobsList = listOf<ReedJobObject>()
lateinit var driver: ChromeDriver
lateinit var wait: WebDriverWait
public fun main(args: Array<String>){
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")
}
}

View File

@@ -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)
}
}

View File

@@ -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<ReedJobObject>?{
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
}
}

View File

@@ -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<ReedResponse>
//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)
}
}
}

View File

@@ -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<T: Any> apiRequest(call: suspend () -> Response<T>) : 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())
}
}
}

View File

@@ -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?
)

View File

@@ -0,0 +1,8 @@
import api.network.responses.ReedJobObject
data class ReedResponse (
val results : List<ReedJobObject>?,
val totalResults : Int?
)