This commit is contained in:
2025-01-18 14:18:17 +01:00
commit abe1a4c515
13 changed files with 1289 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
package nl.veenm.jobfindr.domain;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
public class Vacature {
@Id
@GeneratedValue
private Long id;
private String titel;
private String locatie;
private String url;
private LocalDate datum;
@OneToOne
@JoinColumn(name = "detail_id")
private VacatureDetail detail;
public VacatureDetail getDetail() {
return detail;
}
public void setDetail(VacatureDetail detail) {
this.detail = detail;
}
public Vacature(String title, String link, String location) {
this.titel = title;
this.url = link;
this.locatie = location;
}
public Vacature() {
}
public String getTitel() {
return titel;
}
public String getLocatie() {
return locatie;
}
public String getUrl() {
return url;
}
public void setDatum(LocalDate datum) {
this.datum = datum;
}
}

View File

@@ -0,0 +1,113 @@
package nl.veenm.jobfindr.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class VacatureDetail {
@Id
@GeneratedValue
private Long id;
private String geplaatst;
private String sluitingsdatum;
private String salaris;
private String aantalUren;
private String fte;
private String startdatum;
private String soortVacature;
private String dienstverband;
private String zijInstromers;
public VacatureDetail() {
}
public String getGeplaatst() {
return geplaatst;
}
public void setGeplaatst(String geplaatst) {
this.geplaatst = geplaatst;
}
public String getSluitingsdatum() {
return sluitingsdatum;
}
public void setSluitingsdatum(String sluitingsdatum) {
this.sluitingsdatum = sluitingsdatum;
}
public String getSalaris() {
return salaris;
}
public void setSalaris(String salaris) {
this.salaris = salaris;
}
public String getAantalUren() {
return aantalUren;
}
public void setAantalUren(String aantalUren) {
this.aantalUren = aantalUren;
}
public String getFte() {
return fte;
}
public void setFte(String fte) {
this.fte = fte;
}
public String getStartdatum() {
return startdatum;
}
public void setStartdatum(String startdatum) {
this.startdatum = startdatum;
}
public String getSoortVacature() {
return soortVacature;
}
public void setSoortVacature(String soortVacature) {
this.soortVacature = soortVacature;
}
public String getDienstverband() {
return dienstverband;
}
public void setDienstverband(String dienstverband) {
this.dienstverband = dienstverband;
}
public String getZijInstromers() {
return zijInstromers;
}
public void setZijInstromers(String zijInstromers) {
this.zijInstromers = zijInstromers;
}
@Override
public String toString() {
return "VacatureDetail{" +
"Geplaatst='" + geplaatst + '\'' +
", Sluitingsdatum='" + sluitingsdatum + '\'' +
", Salaris='" + salaris + '\'' +
", Aantal uren='" + aantalUren + '\'' +
", FTE='" + fte + '\'' +
", Startdatum='" + startdatum + '\'' +
", Soort vacature='" + soortVacature + '\'' +
", Dienstverband='" + dienstverband + '\'' +
", Zij-instromers='" + zijInstromers + '\'' +
'}';
}
}

View File

@@ -0,0 +1,15 @@
package nl.veenm.jobfindr.repository;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import nl.veenm.jobfindr.domain.VacatureDetail;
import java.time.LocalDate;
import java.util.List;
@ApplicationScoped
public class VacatureDetailRepository implements PanacheRepository<VacatureDetail> {
}

View File

@@ -0,0 +1,18 @@
package nl.veenm.jobfindr.repository;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import nl.veenm.jobfindr.domain.Vacature;
import java.time.LocalDate;
import java.util.List;
@ApplicationScoped
public class VacatureRepository implements PanacheRepository<Vacature> {
public List<Vacature> findByDate(LocalDate date) {
return list("datum", date);
}
}

View File

@@ -0,0 +1,36 @@
package nl.veenm.jobfindr.resources;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import nl.veenm.jobfindr.domain.Vacature;
import nl.veenm.jobfindr.services.VacatureService;
import java.io.IOException;
import java.util.List;
@Path("/vacatures")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class VacatureResource {
@Inject
VacatureService vacatureService;
@GET
public List<Vacature> getVacatures() throws IOException {
return vacatureService.getServices();
}
@GET()
@Path("/all")
public List<Vacature> getAllVacatures() throws IOException {
return vacatureService.getAllVacatures();
}
@GET
@Path("/detail")
public Vacature getVacature() throws IOException {
return vacatureService.getVacature();
}
}

View File

@@ -0,0 +1,89 @@
package nl.veenm.jobfindr.scrapers;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import nl.veenm.jobfindr.domain.Vacature;
import nl.veenm.jobfindr.domain.VacatureDetail;
import nl.veenm.jobfindr.repository.VacatureDetailRepository;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
@ApplicationScoped
public class ScraperService {
public List<Vacature> scrapeVacatures(String url) throws IOException {
// Maak verbinding met de website
Document doc = Jsoup.connect(url).get();
// Selecteer alle vacature-elementen
Elements vacatureElements = doc.select("div.mt-2.text-overflow-ellipsis");
List<Vacature> vacatures = new ArrayList<>();
for (Element vacatureElement : vacatureElements) {
// Haal de titel en de link op
String title = vacatureElement.select("h4.small-header").text();
String link = vacatureElement.select("a.vacature-target").attr("data-target").replace("/split", "");
// Zorg ervoor dat de link volledig is
if (!link.startsWith("http")) {
link = "https://www.meesterbaan.nl" + link;
}
// Haal de locatie op (zoek naar het eerstvolgende div met class 'location mt-1')
Element locationElement = vacatureElement.parent().selectFirst("div.location.mt-1");
String location = locationElement != null ? locationElement.text() : "Onbekende locatie";
// Maak een Vacature-object en voeg het toe aan de lijst
vacatures.add(new Vacature(title, link, location));
}
vacatures.forEach(vacature -> {
try {
getVacatureDetails(vacature);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return vacatures.stream().filter(a -> !a.getLocatie().equals("Onbekende locatie")).collect(Collectors.toList());
}
public Vacature getVacatureDetails(Vacature vacature) throws IOException {
Document doc = Jsoup.connect(vacature.getUrl()).get();
Elements vacatureElements = doc.select("div.list-property");
HashMap<String, String> details = new HashMap<>();
vacatureElements.forEach(element -> {
String detail = element.selectFirst("label").text();
String value = element.selectXpath("div").text();
details.put(detail, value);
});
VacatureDetail detail = new VacatureDetail();
detail.setGeplaatst(details.get("Geplaatst") != null? details.get("Geplaatst") : "Onbekend");
detail.setSluitingsdatum(details.get("Sluitingsdatum")!= null? details.get("Sluitingsdatum") : "Onbekend");
detail.setSalaris(details.get("Salaris")!= null? details.get("Salaris") : "Onbekend");
detail.setAantalUren(details.get("Aantal uren")!= null? details.get("Aantal uren") : "Onbekend");
detail.setFte(details.get("FTE")!= null? details.get("FTE") : "Onbekend");
detail.setStartdatum(details.get("Startdatum")!= null? details.get("Startdatum") : "Onbekend");
detail.setSoortVacature(details.get("Soort vacature")!= null? details.get("Soort vacature") : "Onbekend");
detail.setDienstverband(details.get("Dienstverband")!= null? details.get("Dienstverband") : "Onbekend");
detail.setZijInstromers(details.get("Zij-instromers")!= null? details.get("Zij-instromers") : "Onbekend");
vacature.setDetail(detail);
return vacature;
}
}

View File

@@ -0,0 +1,57 @@
package nl.veenm.jobfindr.services;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer;
import jakarta.enterprise.context.ApplicationScoped;
import nl.veenm.jobfindr.domain.Vacature;
import java.time.LocalDateTime;
import java.util.List;
@ApplicationScoped
public class EmailService {
private final Mailer mailer;
public EmailService(Mailer mailer) {
this.mailer = mailer;
}
public void stuurVacatureEmail(String recipient, List<Vacature> vacatures) {
// Bouw de inhoud van de e-mail
StringBuilder emailBody = new StringBuilder();
emailBody.append("Lieve Danthe,\n\n");
emailBody.append("Hier zijn de nieuwste vacatures die ik heb gevonden:\n\n");
for (Vacature vacature : vacatures) {
emailBody.append("- ").append(vacature.getTitel()).append("\n");
emailBody.append(" Locatie: ").append(vacature.getLocatie()).append("\n");
emailBody.append(" Details: ").append("\n");
emailBody.append(" -Geplaatst: ").append(vacature.getDetail().getGeplaatst()).append("\n");
emailBody.append(" -Sluitingsdatum: ").append(vacature.getDetail().getSluitingsdatum()).append("\n");
emailBody.append(" -Salaris: ").append(vacature.getDetail().getSalaris()).append("\n");
emailBody.append(" -Aantal uren: ").append(vacature.getDetail().getAantalUren()).append("\n");
emailBody.append(" -FTE: ").append(vacature.getDetail().getFte()).append("\n");
emailBody.append(" -Startdatum: ").append(vacature.getDetail().getStartdatum()).append("\n");
emailBody.append(" -Soort vacature: ").append(vacature.getDetail().getSoortVacature()).append("\n");
emailBody.append(" -Dienstverband: ").append(vacature.getDetail().getDienstverband()).append("\n");
emailBody.append(" -Zij-instromers: ").append(vacature.getDetail().getZijInstromers()).append("\n");
emailBody.append(" Link: ").append(vacature.getUrl()).append("\n\n");
}
emailBody.append("Met vriendelijke groet,\n");
emailBody.append("Het vacatureteam\n");
emailBody.append("(a.k.a je vriendje)");
LocalDateTime now = LocalDateTime.now();
String date = now.getDayOfYear() + "-" + now.getMonth().getValue();
String subject = "[%s] Nieuwe vacatures gevonden";
// Verstuur de e-mail
mailer.send(Mail.withText(recipient, String.format(subject, date), emailBody.toString()));
}
}

View File

@@ -0,0 +1,104 @@
package nl.veenm.jobfindr.services;
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import nl.veenm.jobfindr.domain.Vacature;
import nl.veenm.jobfindr.repository.VacatureDetailRepository;
import nl.veenm.jobfindr.repository.VacatureRepository;
import nl.veenm.jobfindr.scrapers.ScraperService;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@ApplicationScoped
public class VacatureService {
@Inject
ScraperService scraperService;
@Inject
EmailService emailService;
@Inject
VacatureRepository vacatureRepository;
@Inject
VacatureDetailRepository vacatureDetailRepository;
public List<Vacature> getServices() throws IOException {
List<Vacature> vacatures = new ArrayList<>();
vacatures.addAll(scraperService.scrapeVacatures("https://www.meesterbaan.nl/vacatures/50-km?trefwoord=duits&locatie=Apeldoorn"));
vacatures.addAll(scraperService.scrapeVacatures("https://www.meesterbaan.nl/vacatures/50-km?trefwoord=frans&locatie=Apeldoorn"));
emailService.stuurVacatureEmail("vanveenmel11@gmail.com", vacatures);
// emailService.stuurVacatureEmail("danthefranken@gmail.com", vacatures);
return vacatures;
}
public void checkAndSendNewVacatures() throws IOException {
LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);
// Scrape de nieuwste vacatures
String url = "https://www.meesterbaan.nl/vacatures/50-km?trefwoord=duits&locatie=Apeldoorn";
List<Vacature> todayVacatures = scraperService.scrapeVacatures(url);
url = "https://www.meesterbaan.nl/vacatures/50-km?trefwoord=frans&locatie=Apeldoorn";
todayVacatures.addAll(scraperService.scrapeVacatures(url));
todayVacatures.forEach(todayVacature -> {
try {
scraperService.getVacatureDetails(todayVacature);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
// Haal de vacatures van gisteren op
List<Vacature> yesterdayVacatures = vacatureRepository.findByDate(yesterday);
// Filter nieuwe vacatures
List<Vacature> newVacatures = todayVacatures.stream()
.filter(vacature -> yesterdayVacatures.stream()
.noneMatch(yv -> yv.getTitel().equals(vacature.getTitel()) && yv.getUrl().equals(vacature.getUrl())))
.collect(Collectors.toList());
// Alleen nieuwe vacatures opslaan en versturen
if (!newVacatures.isEmpty()) {
// Sla de nieuwe vacatures op
newVacatures.forEach(vacature -> {
System.out.println(newVacatures.size());
vacature.setDatum(today);
vacatureDetailRepository.persist(vacature.getDetail());
vacatureRepository.persist(vacature);
});
// Verstuur een e-mail
// emailService.stuurVacatureEmail("danthefranken@gmail.com", newVacatures);
emailService.stuurVacatureEmail("vanveenmel11@gmail.com", newVacatures);
System.out.println("Nieuwe vacatures verstuurd en opgeslagen.");
} else {
System.out.println("Geen nieuwe vacatures gevonden.");
}
}
@Scheduled(cron = "0 0 9 * * ?")
@Scheduled(cron = "0 53 13 * * ?")
@Scheduled(cron = "0 0 17 * * ?")
@Transactional
void dagelijksControleerEnVerstuur() throws IOException {
checkAndSendNewVacatures();
}
public List<Vacature> getAllVacatures() {
return vacatureRepository.listAll();
}
public Vacature getVacature() throws IOException {
return scraperService.getVacatureDetails(new Vacature());
}
}