V2 FOR MAH BABY

This commit is contained in:
2026-03-01 21:09:36 +01:00
parent f5c0930494
commit e0e0b38367
8 changed files with 269 additions and 119 deletions

View File

@@ -0,0 +1,97 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./mvnw package
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/jobfindr-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/jobfindr-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/jobfindr-jvm
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class")
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
# used to calculate a default maximal heap memory based on a containers restriction.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
# of the container available memory as set here. The default is `50` which means 50%
# of the available memory is used as an upper boundary. You can skip this mechanism by
# setting this value to `0` in which case no `-Xmx` option is added.
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
# is used to calculate a default initial heap memory based on the maximum heap memory.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
# is used as the initial heap size. You can skip this mechanism by setting this value
# to `0` in which case no `-Xms` option is added (example: "25")
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
# This is used to calculate the maximum value of the initial heap memory. If used in
# a container without any memory constraints for the container then this option has
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
# here. The default is 4096MB which means the calculated value of `-Xms` never will
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
# when things are happening. This option, if set to true, will set
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
# true").
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
# (example: "20")
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
# (example: "40")
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
# (example: "4")
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
# previous GC times. (example: "90")
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
# contain the necessary JRE command-line options to specify the required GC, which
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi8/openjdk-21:1.20
ENV LANGUAGE='en_US:en'
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 target/quarkus-app/*.jar /deployments/
COPY --chown=185 target/quarkus-app/app/ /deployments/app/
COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]

View File

@@ -11,7 +11,6 @@ public class Vacature {
@Id
@GeneratedValue
private Long id;
private String titel;
private String locatie;
private String url;
@@ -19,6 +18,8 @@ public class Vacature {
@OneToOne
@JoinColumn(name = "detail_id")
private VacatureDetail detail;
private String description;
private LocalDate closingDate;
public VacatureDetail getDetail() {
return detail;
@@ -53,5 +54,45 @@ public class Vacature {
public void setDatum(LocalDate datum) {
this.datum = datum;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setTitel(String titel) {
this.titel = titel;
}
public void setLocatie(String locatie) {
this.locatie = locatie;
}
public void setUrl(String url) {
this.url = url;
}
public LocalDate getDatum() {
return datum;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public LocalDate getClosingDate() {
return closingDate;
}
public void setClosingDate(LocalDate closingDate) {
this.closingDate = closingDate;
}
}

View File

@@ -6,7 +6,6 @@ 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")
@@ -18,25 +17,25 @@ public class VacatureResource {
VacatureService vacatureService;
@GET
public List<Vacature> getVacatures() throws IOException {
public List<Vacature> getVacatures() {
return vacatureService.getServices();
}
@GET
@Path("/test")
public void testEmails(){
vacatureService.checkAndSendNewVacatures();
}
@GET()
@Path("/all")
public List<Vacature> getAllVacatures() throws IOException {
public List<Vacature> getAllVacatures() {
return vacatureService.getAllVacatures();
}
@GET
@Path("/detail")
public Vacature getVacature() throws IOException {
return vacatureService.getVacature();
}
@GET
@PATCH
@Path("/clean")
public void cleanVacatures() throws IOException {
public void cleanVacatures() {
vacatureService.cleanVacatures();
}
}

View File

@@ -1,10 +1,7 @@
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.jboss.logging.Logger;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
@@ -12,8 +9,9 @@ import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
@@ -22,6 +20,8 @@ public class ScraperService {
private static final Logger log = Logger.getLogger(ScraperService.class);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
public List<Vacature> scrapeVacatures(String url) throws IOException {
// Maak verbinding met de website
Document doc = Jsoup.connect(url).get();
@@ -34,7 +34,7 @@ public class ScraperService {
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 title = vacatureElement.select("div.heading-6").text();
String link = vacatureElement.select("a.vacature-target").attr("data-target").replace("/split", "");
// Zorg ervoor dat de link volledig is
@@ -42,7 +42,7 @@ public class ScraperService {
link = "https://www.meesterbaan.nl" + link;
}
// Haal de locatie op (zoek naar het eerstvolgende div met class 'location mt-1')
assert vacatureElement.parent() != null;
Element locationElement = vacatureElement.parent().selectFirst("div.location.mt-1");
String location = locationElement != null ? locationElement.text() : "Onbekende locatie";
@@ -61,34 +61,19 @@ public class ScraperService {
return vacatures.stream().filter(a -> !a.getLocatie().equals("Onbekende locatie")).collect(Collectors.toList());
}
public Vacature getVacatureDetails(Vacature vacature) throws IOException {
public void 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<>();
Elements vacatureElements = doc.select("div.rounded.gray-tag-md.px-2.py-2");
StringBuilder sb = new StringBuilder();
vacatureElements.forEach(element -> {
String detail = element.selectFirst("label").text();
String value = element.selectXpath("div").text();
details.put(detail, value);
sb.append(element.text()).append("<br>");
if (element.text().startsWith("Sluitingsdatum:")) {
vacature.setClosingDate(LocalDate.parse(element.text().replace("Sluitingsdatum: ", ""), formatter));
}
});
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;
vacature.setDescription(sb.toString());
}
}

View File

@@ -33,46 +33,39 @@ public class EmailService {
emailBody.append("<html>")
.append("<head>")
.append("<style>")
.append("body { font-family: Arial, sans-serif; line-height: 1.6; }")
.append("h1 { color: #333; }")
.append("h2 { color: #555; }")
.append("p { color: #666; }")
.append("ul { padding-left: 20px; }")
.append("li { margin-bottom: 10px; }")
.append(".vacature { margin-bottom: 20px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; background-color: #f9f9f9; }")
.append(".vacature h2 { margin: 0; font-size: 18px; color: #333; }")
.append(".vacature p { margin: 5px 0; }")
.append(".footer { margin-top: 30px; font-size: 14px; color: #999; }")
.append("body { font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background-color: #f4f7f6; margin: 0; padding: 20px; -webkit-font-smoothing: antialiased; }")
.append(".email-container { max-width: 600px; margin: 0 auto; background-color: #ffffff; padding: 40px; border-radius: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }")
.append("h1 { color: #2d3748; font-size: 26px; margin-top: 0; }")
.append(".intro { color: #718096; font-size: 16px; margin-bottom: 30px; }")
.append(".vacature { margin-bottom: 25px; padding: 25px; border: 1px solid #edf2f7; border-radius: 12px; background-color: #ffffff; box-shadow: 0 2px 5px rgba(0,0,0,0.02); }")
.append(".vacature h2 { margin: 0 0 10px 0; font-size: 20px; color: #2b6cb0; }")
.append(".vacature p { color: #4a5568; line-height: 1.6; margin-bottom: 20px; font-size: 15px; }")
.append(".label { font-weight: 600; color: #a0aec0; text-transform: uppercase; letter-spacing: 1px; font-size: 12px; margin-bottom: 5px; display: block; }")
.append(".footer { margin-top: 40px; padding-top: 20px; border-top: 2px dashed #edf2f7; font-size: 14px; color: #a0aec0; text-align: center; line-height: 1.5; }")
.append("</style>")
.append("</head>")
.append("<body>")
.append("<h1>Lieve Danthe,</h1>")
.append("<p>Hier zijn de nieuwste vacatures die ik heb gevonden:</p>")
.append("<div>");
.append("<div class=\"email-container\">")
.append("<h1>Lieve Danthe, ✨</h1>")
.append("<p class=\"intro\">Ik heb weer even voor je gezocht. Hier zijn de nieuwste vacatures die perfect bij jou passen:</p>");
for (Vacature vacature : vacatures) {
emailBody.append("<div class=\"vacature\">")
.append("<h2>").append(vacature.getTitel()).append("</h2>")
.append("<p><strong>Locatie:</strong> ").append(vacature.getLocatie()).append("</p>")
.append("<p><strong>Geplaatst:</strong> ").append(vacature.getDetail().getGeplaatst()).append("</p>")
.append("<p><strong>Sluitingsdatum:</strong> ").append(vacature.getDetail().getSluitingsdatum()).append("</p>")
.append("<p><strong>Salaris:</strong> ").append(vacature.getDetail().getSalaris()).append("</p>")
.append("<p><strong>Aantal uren:</strong> ").append(vacature.getDetail().getAantalUren()).append("</p>")
.append("<p><strong>FTE:</strong> ").append(vacature.getDetail().getFte()).append("</p>")
.append("<p><strong>Startdatum:</strong> ").append(vacature.getDetail().getStartdatum()).append("</p>")
.append("<p><strong>Soort vacature:</strong> ").append(vacature.getDetail().getSoortVacature()).append("</p>")
.append("<p><strong>Dienstverband:</strong> ").append(vacature.getDetail().getDienstverband()).append("</p>")
.append("<p><strong>Zij-instromers:</strong> ").append(vacature.getDetail().getZijInstromers()).append("</p>")
.append("<p><a href=\"").append(vacature.getUrl()).append("\" style=\"color: #007bff; text-decoration: none;\">Bekijk vacature</a></p>")
.append("<span class=\"label\">Korte beschrijving</span>")
.append("<p>").append(vacature.getDescription()).append("</p>")
.append("<div><a href=\"").append(vacature.getUrl())
.append("\" style=\"display: inline-block; padding: 12px 24px; background-color: #ed8936; color: #ffffff; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 14px;\">")
.append("Bekijk vacature &rarr;</a></div>")
.append("</div>");
}
emailBody.append("</div>")
.append("<div class=\"footer\">")
.append("Met vriendelijke groet,<br>")
.append("Het vacatureteam<br>")
.append("(a.k.a je vriendje)")
emailBody.append("<div class=\"footer\">")
.append("Met heel veel liefde gebouwd,<br>")
.append("<strong>Het vacatureteam</strong><br>")
.append("(a.k.a. je vriendje ❤️)")
.append("</div>")
.append("</div>") // Einde email-container
.append("</body>")
.append("</html>");

View File

@@ -13,8 +13,6 @@ import org.jboss.logging.Logger;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
@@ -39,26 +37,19 @@ public class VacatureService {
private final String className = this.getClass().getSimpleName();
public List<Vacature> getServices() throws IOException {
public List<Vacature> getServices() {
eventService.logInfo(VacatureService.class.getName(), "getServices", "getServices aangeroepen");
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"));
List<Vacature> vacatures = getVacatures();
emailService.stuurVacatureEmail("vanveenmel11@gmail.com", vacatures);
return vacatures;
}
public void checkAndSendNewVacatures() throws IOException {
@Transactional
public void checkAndSendNewVacatures() {
logger.info("Checking for new vacatures");
eventService.logInfo(className, "checkAndSendNewVacatures", "Checking for new vacatures");
LocalDate today = LocalDate.now();
// 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));
List<Vacature> todayVacatures = getVacatures();
todayVacatures.forEach(todayVacature -> {
try {
@@ -71,25 +62,18 @@ public class VacatureService {
// Haal de vacatures van gisteren op
List<Vacature> yesterdayVacatures = vacatureRepository.findAll().stream().toList();
System.out.println(todayVacatures.size());
System.out.println(yesterdayVacatures.size());
todayVacatures.removeIf(a -> yesterdayVacatures.stream().anyMatch(b -> b.getUrl().equals(a.getUrl())));
System.out.println(todayVacatures.size());
// Alleen nieuwe vacatures opslaan en versturen
if (!todayVacatures.isEmpty()) {
// Sla de nieuwe vacatures op
todayVacatures.forEach(vacature -> {
vacature.setDatum(today);
vacatureDetailRepository.persist(vacature.getDetail());
vacatureRepository.persist(vacature);
});
// Verstuur een e-mail
emailService.stuurVacatureEmail("danthefranken@gmail.com", todayVacatures);
// emailService.stuurVacatureEmail("danthefranken@gmail.com", todayVacatures);
emailService.stuurVacatureEmail("vanveenmel11@gmail.com", todayVacatures);
logger.info("Nieuwe vacatures verstuurd en opgeslagen.");
eventService.logSucces(className, "checkAndSendNewVacatures", "Nieuwe vacatures verstuurd en opgeslagen.");
@@ -99,10 +83,28 @@ public class VacatureService {
}
}
private List<Vacature> getVacatures() {
List<String> urls = List.of(
"https://www.meesterbaan.nl/vacatures/plaats/apeldoorn/35-km?trefwoord=geschiedenis",
"https://www.meesterbaan.nl/vacatures/plaats/apeldoorn/35-km?trefwoord=Docent%20Geschiedenis"
);
List<Vacature> todayVacatures = new ArrayList<>();
// Scrape de nieuwste vacatures
urls.forEach(url -> {
try {
todayVacatures.addAll(scraperService.scrapeVacatures(url));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return todayVacatures;
}
@Scheduled(cron = "0 0 9 * * ?")
@Scheduled(cron = "0 0 17 * * ?")
@Transactional
void dagelijksControleerEnVerstuur() throws IOException {
void dagelijksControleerEnVerstuur() {
checkAndSendNewVacatures();
}
@@ -111,41 +113,24 @@ public class VacatureService {
return vacatureRepository.listAll();
}
public Vacature getVacature() throws IOException {
return scraperService.getVacatureDetails(new Vacature());
}
@Scheduled(cron = "0 0 0 * * ?")
@Transactional
public void cleanVacatures() {
logger.info("Cleaning vacatures");
eventService.logInfo(className, "cleanVacatures", "Cleaning vacatures");
getAllVacatures().forEach(vacature -> {
var date = convertStringToLocalDate(vacature.getDetail().getSluitingsdatum());
var date = vacature.getClosingDate();
var today = LocalDate.now();
if (date != null) {
if (date.isBefore(today)) {
logger.info("Deleting vacature " + vacature.getTitel());
eventService.logInfo(className, "cleanVacatures", "Deleting vacature " + vacature.getTitel());
vacatureDetailRepository.delete(vacature.getDetail());
vacatureRepository.delete(vacature);
}
if (date.isBefore(today)) {
logger.info("Deleting vacature " + vacature.getTitel());
eventService.logInfo(className, "cleanVacatures", "Deleting vacature " + vacature.getTitel());
vacatureDetailRepository.delete(vacature.getDetail());
vacatureRepository.delete(vacature);
}
eventService.logSucces(className, "cleanVacatures", "Cleaning vacatures done");
});
logger.info("Cleaning vacatures done");
eventService.logSucces(className, "cleanVacatures", "Cleaning vacatures done");
}
public static LocalDate convertStringToLocalDate(String dateString) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
try {
return LocalDate.parse(dateString, formatter);
} catch (DateTimeParseException e) {
logger.error("Ongeldige datumstring: " + dateString);
return null; // Of gooi een aangepaste exception als dat gewenst is
}
}
}