diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs index e479558..e889521 100644 --- a/.settings/org.eclipse.buildship.core.prefs +++ b/.settings/org.eclipse.buildship.core.prefs @@ -1,13 +1,2 @@ -arguments= -auto.sync=false -build.scans.enabled=false -connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) connection.project.dir= eclipse.preferences.version=1 -gradle.user.home= -java.home= -jvm.arguments= -offline.mode=false -override.workspace.settings=false -show.console.view=false -show.executions.view=false diff --git a/.settings/org.eclipse.wst.common.component b/.settings/org.eclipse.wst.common.component index a2ed0c0..59fc2ab 100644 --- a/.settings/org.eclipse.wst.common.component +++ b/.settings/org.eclipse.wst.common.component @@ -1,7 +1,7 @@ - - + + diff --git a/build.gradle b/build.gradle index b63b910..af78b06 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,13 @@ dependencies { // backward compatibility until the complete registration is converted to keycloak implementation 'org.jasypt:jasypt:1.9.3' + // rss support + implementation 'com.rometools:rome:1.18.0' + + // mail support + implementation 'commons-validator:commons-validator:1.7' + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-jooq' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' diff --git a/src/main/java/de/jottyfan/camporganizer/module/common/IndexGateway.java b/src/main/java/de/jottyfan/camporganizer/module/common/IndexGateway.java index 046e056..ea15b5f 100644 --- a/src/main/java/de/jottyfan/camporganizer/module/common/IndexGateway.java +++ b/src/main/java/de/jottyfan/camporganizer/module/common/IndexGateway.java @@ -16,13 +16,11 @@ import org.jooq.DSLContext; import org.jooq.Record; import org.jooq.SelectSeekStep1; import org.jooq.SelectSeekStep2; -import org.jooq.UpdateConditionStep; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import de.jottyfan.camporganizer.db.jooq.tables.TProfile; -import de.jottyfan.camporganizer.db.jooq.tables.records.TPersonRecord; import de.jottyfan.camporganizer.db.jooq.tables.records.VCampRecord; /** @@ -132,28 +130,4 @@ public class IndexGateway { } return list; } - - /** - * update defined fields of the bean - * - * @param bean the bean - * @return number of affected database rows; should be 1 - */ - public Integer update(BookingBean bean) { - UpdateConditionStep sql = jooq - // @formatter:off - .update(T_PERSON) - .set(T_PERSON.FORENAME, bean.getForename()) - .set(T_PERSON.SURNAME, bean.getSurname()) - .set(T_PERSON.STREET, bean.getStreet()) - .set(T_PERSON.ZIP, bean.getZip()) - .set(T_PERSON.CITY, bean.getCity()) - .set(T_PERSON.PHONE, bean.getPhone()) - .set(T_PERSON.EMAIL, bean.getEmail()) - .set(T_PERSON.COMMENT, bean.getComment()) - .where(T_PERSON.PK.eq(bean.getPk())); - // @formatter:on - LOGGER.debug(sql.toString()); - return sql.execute(); - } } diff --git a/src/main/java/de/jottyfan/camporganizer/module/common/IndexService.java b/src/main/java/de/jottyfan/camporganizer/module/common/IndexService.java index 878d5c8..0fb8c47 100644 --- a/src/main/java/de/jottyfan/camporganizer/module/common/IndexService.java +++ b/src/main/java/de/jottyfan/camporganizer/module/common/IndexService.java @@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import de.jottyfan.camporganizer.db.jooq.tables.records.VCampRecord; +import de.jottyfan.camporganizer.module.dashboard.DashboardGateway; /** * @@ -21,9 +22,13 @@ import de.jottyfan.camporganizer.db.jooq.tables.records.VCampRecord; */ @Service public class IndexService { + @Autowired private IndexGateway gateway; + @Autowired + private DashboardGateway dashboardGateway; + /** * get all camps from the database and prepare them for the view * @@ -54,6 +59,6 @@ public class IndexService { * @return true or false */ public Boolean update(BookingBean bean) { - return gateway.update(bean) == 1; + return dashboardGateway.update(bean) == 1; } } diff --git a/src/main/java/de/jottyfan/camporganizer/module/common/LambdaResultWrapper.java b/src/main/java/de/jottyfan/camporganizer/module/common/LambdaResultWrapper.java index 4f38ea0..2bc8c71 100644 --- a/src/main/java/de/jottyfan/camporganizer/module/common/LambdaResultWrapper.java +++ b/src/main/java/de/jottyfan/camporganizer/module/common/LambdaResultWrapper.java @@ -1,14 +1,21 @@ package de.jottyfan.camporganizer.module.common; +import java.util.HashMap; +import java.util.Map; + /** - * + * * @author henkej * */ public class LambdaResultWrapper { private Integer counter; - + private Map mapBoolean; + private Map mapString; + public LambdaResultWrapper() { + this.mapBoolean = new HashMap<>(); + this.mapString = new HashMap<>(); counter = 0; } @@ -19,4 +26,20 @@ public class LambdaResultWrapper { public void add(Integer i) { counter += i; } + + public void putBoolean(String key, Boolean value) { + mapBoolean.put(key, value); + } + + public Boolean getBoolean(String key) { + return mapBoolean.get(key); + } + + public void putString(String key, String value) { + mapString.put(key, value); + } + + public String getString(String key) { + return mapString.get(key); + } } diff --git a/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/IPersonService.java b/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/IPersonService.java index a628db7..190eec8 100644 --- a/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/IPersonService.java +++ b/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/IPersonService.java @@ -25,9 +25,10 @@ public interface IPersonService { * update bean in the database * * @param bean the bean + * @param worker the user that is doing the changes * @return number of affected database rows */ - public Integer updatePerson(PersonBean bean); + public Integer updatePerson(PersonBean bean, String worker); /** * get all camps from the database that the user has access to diff --git a/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/PersonController.java b/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/PersonController.java index e4651c6..47a5597 100644 --- a/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/PersonController.java +++ b/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/PersonController.java @@ -42,7 +42,8 @@ public class PersonController { @PostMapping("/confirmation/person/update") public String doUpdate(@ModelAttribute PersonBean bean, Model model) { - personService.updatePerson(bean); + String username = confirmationService.getCurrentUser(request); + personService.updatePerson(bean, username); return "redirect:/confirmation"; } } diff --git a/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/impl/PersonGateway.java b/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/impl/PersonGateway.java index 169e3be..8c5b212 100644 --- a/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/impl/PersonGateway.java +++ b/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/impl/PersonGateway.java @@ -5,23 +5,28 @@ import static de.jottyfan.camporganizer.db.jooq.Tables.T_CAMPPROFILE; import static de.jottyfan.camporganizer.db.jooq.Tables.T_LOCATION; import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSON; import static de.jottyfan.camporganizer.db.jooq.Tables.T_PROFILE; +import static de.jottyfan.camporganizer.db.jooq.Tables.T_RSS; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jooq.DSLContext; +import org.jooq.InsertValuesStep2; import org.jooq.Record; import org.jooq.Record11; import org.jooq.Record4; import org.jooq.SelectConditionStep; import org.jooq.SelectSeekStep1; import org.jooq.UpdateConditionStep; +import org.jooq.impl.DSL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -29,7 +34,11 @@ import org.springframework.transaction.annotation.Transactional; import de.jottyfan.camporganizer.db.jooq.enums.EnumCamprole; import de.jottyfan.camporganizer.db.jooq.enums.EnumModule; import de.jottyfan.camporganizer.db.jooq.tables.TProfile; +import de.jottyfan.camporganizer.db.jooq.tables.records.TCampRecord; import de.jottyfan.camporganizer.db.jooq.tables.records.TPersonRecord; +import de.jottyfan.camporganizer.db.jooq.tables.records.TRssRecord; +import de.jottyfan.camporganizer.module.common.LambdaResultWrapper; +import de.jottyfan.camporganizer.module.mail.MailRepository; /** * @@ -44,6 +53,9 @@ public class PersonGateway { @Autowired private DSLContext jooq; + @Autowired + private MailRepository mailRepository; + /** * get all camps from the database if username is allowed to maintain it * @@ -125,26 +137,119 @@ public class PersonGateway { * @param bean the bean * @return the number of affected database rows */ - public Integer updatePerson(PersonBean bean) { - UpdateConditionStep sql = jooq - // @formatter:off - .update(T_PERSON) - .set(T_PERSON.FORENAME, bean.getForename()) - .set(T_PERSON.SURNAME, bean.getSurname()) - .set(T_PERSON.STREET, bean.getStreet()) - .set(T_PERSON.ZIP, bean.getZip()) - .set(T_PERSON.CITY, bean.getCity()) - .set(T_PERSON.BIRTHDATE, bean.getBirthdate()) - .set(T_PERSON.SEX, bean.getSex()) - .set(T_PERSON.PHONE, bean.getPhone()) - .set(T_PERSON.EMAIL, bean.getEmail()) - .set(T_PERSON.COMMENT, bean.getComment()) - .set(T_PERSON.ACCEPT, bean.getAccept()) - .set(T_PERSON.CAMPROLE, bean.getCamprole()) - .where(T_PERSON.PK.eq(bean.getPk())); - // @formatter:on - LOGGER.debug(sql.toString()); - return sql.execute(); + public Integer updatePerson(PersonBean bean, String registrator) { + LambdaResultWrapper lrw = new LambdaResultWrapper(); + jooq.transaction(t -> { + + // get old accept value for comparison + SelectConditionStep sql = jooq.selectFrom(T_PERSON).where(T_PERSON.PK.eq(bean.getPk())); + LOGGER.debug(sql.toString()); + TPersonRecord r = sql.fetchOne(); + lrw.putBoolean("acceptOld", r == null ? null : r.getAccept()); + lrw.putBoolean("acceptNew", bean.getAccept()); + Integer fkCamp = r == null ? null : r.getFkCamp(); + String email = r.getEmail(); // use the old one, too + lrw.putString("oldEmail", email); + + SelectConditionStep sql0 = jooq.selectFrom(T_CAMP).where(T_CAMP.PK.eq(fkCamp)); + LOGGER.debug(sql0.toString()); + TCampRecord rc = sql0.fetchOne(); + String campName = rc == null ? null : rc.getName(); + LocalDateTime arrive = rc == null ? null : rc.getArrive(); + String campNameWithYear = new StringBuilder(campName == null ? "" : campName).append(" ") + .append(arrive == null ? "" : arrive.format(DateTimeFormatter.ofPattern("YYYY"))).toString(); + lrw.putString("campNameWithYear", campNameWithYear); + + UpdateConditionStep sql1 = jooq + // @formatter:off + .update(T_PERSON) + .set(T_PERSON.FORENAME, bean.getForename()) + .set(T_PERSON.SURNAME, bean.getSurname()) + .set(T_PERSON.STREET, bean.getStreet()) + .set(T_PERSON.ZIP, bean.getZip()) + .set(T_PERSON.CITY, bean.getCity()) + .set(T_PERSON.BIRTHDATE, bean.getBirthdate()) + .set(T_PERSON.SEX, bean.getSex()) + .set(T_PERSON.PHONE, bean.getPhone()) + .set(T_PERSON.EMAIL, bean.getEmail()) + .set(T_PERSON.COMMENT, bean.getComment()) + .set(T_PERSON.ACCEPT, bean.getAccept()) + .set(T_PERSON.CAMPROLE, bean.getCamprole()) + .where(T_PERSON.PK.eq(bean.getPk())); + // @formatter:on + LOGGER.debug(sql1.toString()); + lrw.add(sql1.execute()); + + // always + StringBuilder buf = new StringBuilder("Eine Anmeldung für "); + buf.append(campNameWithYear); + buf.append(" wurde von ").append(registrator); + buf.append(" korrigiert."); + + InsertValuesStep2 sql2 = DSL.using(t) + // @formatter:off + .insertInto(T_RSS, + T_RSS.MSG, + T_RSS.RECIPIENT) + .values(buf.toString(), "registrator"); + // @formatter:on + LOGGER.debug("{}", sql2.toString()); + sql2.execute(); + }); + + // send email to user instead of rss feed + Boolean acceptNew = lrw.getBoolean("acceptNew"); + Boolean acceptOld = lrw.getBoolean("acceptOld"); + String campNameWithYear = lrw.getString("campNameWithYear"); + String email = lrw.getString("oldEmail"); + StringBuilder buf = new StringBuilder(); + if (acceptNew == null) { + if (acceptOld != null) { + buf = new StringBuilder("Die Bestätigung der Anmeldung von "); + buf.append(bean.getForename()); + buf.append(" "); + buf.append(bean.getSurname()); + buf.append(" zur Freizeit "); + buf.append(campNameWithYear); + buf.append(" wurde von "); + buf.append(registrator); + buf.append(" wieder zurückgezogen."); + buf.append( + " Möglicherweise wurde die Anmeldung versehentlich bestätigt? Deine Anmeldung befindet sich jetzt wieder auf der Warteliste."); + } + } else if (acceptNew == true) { + if (acceptOld == null || !acceptOld) { + buf = new StringBuilder("Die Anmeldung von "); + buf.append(bean.getForename()); + buf.append(" "); + buf.append(bean.getSurname()); + buf.append(" zur Freizeit "); + buf.append(campNameWithYear); + buf.append(" wurde bestätigt. Melde Dich jetzt unter https://www.onkelwernerfreizeiten.de/camporganizer an,"); + buf.append(" um die Bestätigungen herunterzuladen."); + } + } else if (acceptNew == false) { + if (acceptOld == null || acceptOld) { + buf = new StringBuilder("Die Anmeldung von "); + buf.append(bean.getForename()); + buf.append(" "); + buf.append(bean.getSurname()); + buf.append(" zur Freizeit "); + buf.append(campNameWithYear); + buf.append(" wurde leider abgelehnt."); + buf.append( + " Möglicherweise ist sie schon ausgebucht? Deine Anmeldung befindet sich jetzt auf der Nachrückerliste."); + } + } + Set to = new HashSet<>(); + to.add(email); + to.add(bean.getEmail()); + try { + mailRepository.sendMail(to, buf.toString()); // no matter if the sending works, do the persistence anyway + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } + return lrw.getCounter(); } /** diff --git a/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/impl/PersonService.java b/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/impl/PersonService.java index 7bb35d8..5ef776c 100644 --- a/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/impl/PersonService.java +++ b/src/main/java/de/jottyfan/camporganizer/module/confirmation/person/impl/PersonService.java @@ -23,8 +23,8 @@ public class PersonService implements IPersonService { } @Override - public Integer updatePerson(PersonBean bean) { - return gateway.updatePerson(bean); + public Integer updatePerson(PersonBean bean, String worker) { + return gateway.updatePerson(bean, worker); } @Override diff --git a/src/main/java/de/jottyfan/camporganizer/module/dashboard/DashboardGateway.java b/src/main/java/de/jottyfan/camporganizer/module/dashboard/DashboardGateway.java new file mode 100644 index 0000000..d4d1c76 --- /dev/null +++ b/src/main/java/de/jottyfan/camporganizer/module/dashboard/DashboardGateway.java @@ -0,0 +1,137 @@ +package de.jottyfan.camporganizer.module.dashboard; + +import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSON; +import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSONDOCUMENT; +import static de.jottyfan.camporganizer.db.jooq.Tables.T_RSS; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jooq.DSLContext; +import org.jooq.DeleteConditionStep; +import org.jooq.InsertValuesStep2; +import org.jooq.InsertValuesStep4; +import org.jooq.UpdateConditionStep; +import org.jooq.exception.DataAccessException; +import org.jooq.impl.DSL; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import de.jottyfan.camporganizer.db.jooq.enums.EnumFiletype; +import de.jottyfan.camporganizer.db.jooq.tables.records.TPersonRecord; +import de.jottyfan.camporganizer.db.jooq.tables.records.TPersondocumentRecord; +import de.jottyfan.camporganizer.db.jooq.tables.records.TRssRecord; +import de.jottyfan.camporganizer.module.common.BookingBean; +import de.jottyfan.camporganizer.module.common.LambdaResultWrapper; + +/** + * + * @author jotty + * + */ +@Repository +@Transactional(transactionManager = "transactionManager") +public class DashboardGateway { + private static final Logger LOGGER = LogManager.getLogger(DashboardGateway.class); + + @Autowired + private DSLContext jooq; + + /** + * update defined fields of the bean + * + * @param bean the bean + * @return number of affected database rows; should be 1 + */ + public Integer update(BookingBean bean) { + UpdateConditionStep sql = jooq + // @formatter:off + .update(T_PERSON) + .set(T_PERSON.FORENAME, bean.getForename()) + .set(T_PERSON.SURNAME, bean.getSurname()) + .set(T_PERSON.STREET, bean.getStreet()) + .set(T_PERSON.ZIP, bean.getZip()) + .set(T_PERSON.CITY, bean.getCity()) + .set(T_PERSON.PHONE, bean.getPhone()) + .set(T_PERSON.EMAIL, bean.getEmail()) + .set(T_PERSON.COMMENT, bean.getComment()) + .where(T_PERSON.PK.eq(bean.getPk())); + // @formatter:on + LOGGER.debug(sql.toString()); + return sql.execute(); + } + + + /** + * delete entry from t_persondocument where pk = ? + * + * @param pk + * to be used as reference + * @return number of affected database lines + * @throws DataAccessException + */ + public Integer deletePersondocument(PersondocumentBean bean) throws DataAccessException { + LambdaResultWrapper lrw = new LambdaResultWrapper(); + jooq.transaction(t -> { + + DeleteConditionStep sql = DSL.using(t) + // @formatter:off + .deleteFrom(T_PERSONDOCUMENT) + .where(T_PERSONDOCUMENT.PK.eq(bean.getPk())); + // @formatter:on + LOGGER.debug("{}", sql.toString()); + lrw.add(sql.execute()); + + StringBuilder buf = new StringBuilder("Dokument "); + buf.append(bean.getName()); + buf.append(" wurde wieder gelöscht."); + InsertValuesStep2 sql2 = DSL.using(t) + // @formatter:off + .insertInto(T_RSS, + T_RSS.MSG, + T_RSS.RECIPIENT) + .values(buf.toString(), "registrator"); + // @formatter:on + LOGGER.debug("{}", sql2.toString()); + sql2.execute(); + }); + return lrw.getCounter(); + } + + /** + * add document to database + * + * @param bean + * @throws DataAccessException + */ + public void addPersondocument(PersondocumentBean bean) throws DataAccessException { + jooq.transaction(t -> { + + InsertValuesStep4 sql = DSL.using(t) + // @formatter:off + .insertInto(T_PERSONDOCUMENT, + T_PERSONDOCUMENT.NAME, + T_PERSONDOCUMENT.FILETYPE, + T_PERSONDOCUMENT.FK_PERSON, + T_PERSONDOCUMENT.DOCUMENT + ) + .values(bean.getName(), bean.getFiletype(), bean.getFkPerson(), bean.getDocument()); + // @formatter:on + LOGGER.debug("{}", sql.toString()); + sql.execute(); + + StringBuilder buf = new StringBuilder("Dokument "); + buf.append(bean.getName()); + buf.append(" wurde angelegt."); + InsertValuesStep2 sql2 = DSL.using(t) + // @formatter:off + .insertInto(T_RSS, + T_RSS.MSG, + T_RSS.RECIPIENT) + .values(buf.toString(), "registrator"); + // @formatter:on + LOGGER.debug("{}", sql2.toString()); + sql2.execute(); + }); + } +} diff --git a/src/main/java/de/jottyfan/camporganizer/module/dashboard/PersondocumentBean.java b/src/main/java/de/jottyfan/camporganizer/module/dashboard/PersondocumentBean.java new file mode 100644 index 0000000..f00b165 --- /dev/null +++ b/src/main/java/de/jottyfan/camporganizer/module/dashboard/PersondocumentBean.java @@ -0,0 +1,86 @@ +package de.jottyfan.camporganizer.module.dashboard; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Base64; + +import javax.servlet.http.Part; + +import org.apache.commons.io.IOUtils; + +import de.jottyfan.camporganizer.db.jooq.enums.EnumFiletype; + +/** + * + * @author jotty + * + */ +public class PersondocumentBean { + + private final Integer pk; + private Integer fkPerson; + private String name; + private String document; + private EnumFiletype filetype; + private Part uploadfile; + + public PersondocumentBean(Integer pk) { + this.pk = pk; + } + + public void encodeUpload() throws IOException { + if (uploadfile != null) { + InputStream inputStream = uploadfile.getInputStream(); + byte[] bytes = IOUtils.toByteArray(inputStream); + if (bytes.length > 0) { + document = Base64.getEncoder().encodeToString(bytes); + } // not uploaded files should not be changed, so document must be kept as is + } else { + throw new IOException("uploadfile is null"); + } + } + + public Integer getPk() { + return pk; + } + + public void setFkPerson(Integer fkPerson) { + this.fkPerson = fkPerson; + } + + public Integer getFkPerson() { + return fkPerson; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setDocument(String document) { + this.document = document; + } + + public String getDocument() { + return document; + } + + public void setFiletype(EnumFiletype filetype) { + this.filetype = filetype; + } + + public EnumFiletype getFiletype() { + return filetype; + } + + public Part getUploadfile() { + return uploadfile; + } + + public void setUploadfile(Part uploadfile) { + this.uploadfile = uploadfile; + } +} diff --git a/src/main/java/de/jottyfan/camporganizer/module/mail/MailRepository.java b/src/main/java/de/jottyfan/camporganizer/module/mail/MailRepository.java new file mode 100644 index 0000000..06b865a --- /dev/null +++ b/src/main/java/de/jottyfan/camporganizer/module/mail/MailRepository.java @@ -0,0 +1,96 @@ +package de.jottyfan.camporganizer.module.mail; + +import java.nio.charset.StandardCharsets; +import java.util.Set; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Repository; + +/** + * + * @author jotty + * + */ +@Repository +public class MailRepository { + + private final static Logger LOGGER = LogManager.getLogger(); + + @Autowired + private JavaMailSender javaMailSender; + + @Value("${spring.mail.username}") + private String username; + + /** + * Send an email with the message to the recipient. If email is blank, do + * nothing + * + * @param to the email addresses + * @param message the message + */ + public void sendMail(Set to, String message) { + if (to != null && to.size() > 0) { + if (username != null && !username.isBlank()) { + try { + sendMail(to, message, username); + } catch (MessagingException e) { + LOGGER.error(e.getMessage(), e); + } + } else { + LOGGER.error("no email.username in configuration for sending emails"); + } + } else { + LOGGER.warn("no email address given, ignore informing the user about changes; message would have been: {}", + message); + } + } + + /** + * send the email + * + * @param to the recipients + * @param message the message + * @param from the username of the email account + * @throws MessagingException + */ + private void sendMail(Set to, String message, String from) throws MessagingException { + if (to == null || to.size() < 1) { + throw new MessagingException("no recipient in " + to); + } + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, + StandardCharsets.UTF_8.name()); + helper.setFrom(from); + helper.setSubject("Information zu Deiner Anmeldung zur Onkel Werner Freizeit"); + helper.setText(message, false); + helper.setTo(to.toArray(new String[] {})); + javaMailSender.send(mimeMessage); + } + + /** + * for junit tests only + * + * @param javaMailSender the java mail sender + */ + protected void setJavaMailSender(JavaMailSender javaMailSender) { + this.javaMailSender = javaMailSender; + } + + /** + * for junit tests only + * + * @param username the username + */ + protected void setUsername(String username) { + this.username = username; + } +} diff --git a/src/main/java/de/jottyfan/camporganizer/module/registration/ProfileBean.java b/src/main/java/de/jottyfan/camporganizer/module/registration/ProfileBean.java new file mode 100644 index 0000000..8c71dc8 --- /dev/null +++ b/src/main/java/de/jottyfan/camporganizer/module/registration/ProfileBean.java @@ -0,0 +1,64 @@ +package de.jottyfan.camporganizer.module.registration; + +import java.io.Serializable; + +/** + * + * @author jotty + * + */ +public class ProfileBean implements Serializable { + private static final long serialVersionUID = 1L; + + private Integer pk; + private String forename; + private String surname; + private String username; + + public void clear() { + this.pk = null; + this.forename = null; + this.surname = null; + this.username = null; + } + + public Boolean getIsEmpty() { + return pk == null; + } + + public String getFullname() { + return new StringBuilder(forename == null ? "" : forename).append(" ").append(surname == null ? "" : surname) + .toString(); + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getForename() { + return forename; + } + + public void setForename(String forename) { + this.forename = forename; + } + + public String getSurname() { + return surname; + } + + public void setSurname(String surname) { + this.surname = surname; + } + public Integer getPk() { + return pk; + } + + public void setPk(Integer pk) { + this.pk = pk; + } +} diff --git a/src/main/java/de/jottyfan/camporganizer/module/registration/RegistrationBean.java b/src/main/java/de/jottyfan/camporganizer/module/registration/RegistrationBean.java index debd3ac..0181cc7 100644 --- a/src/main/java/de/jottyfan/camporganizer/module/registration/RegistrationBean.java +++ b/src/main/java/de/jottyfan/camporganizer/module/registration/RegistrationBean.java @@ -49,6 +49,13 @@ public class RegistrationBean implements Serializable { private String login; private String password; + /** + * @return forename + surname, separated by a space + */ + public String getFullname() { + return new StringBuilder().append(forename).append(" ").append(surname).toString(); + } + /** * @return the forename */ diff --git a/src/main/java/de/jottyfan/camporganizer/module/registration/RegistrationGateway.java b/src/main/java/de/jottyfan/camporganizer/module/registration/RegistrationGateway.java index e6ab202..0d1486c 100644 --- a/src/main/java/de/jottyfan/camporganizer/module/registration/RegistrationGateway.java +++ b/src/main/java/de/jottyfan/camporganizer/module/registration/RegistrationGateway.java @@ -4,9 +4,12 @@ import static de.jottyfan.camporganizer.db.jooq.Tables.T_CAMP; import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSON; import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSONDOCUMENT; import static de.jottyfan.camporganizer.db.jooq.Tables.T_PROFILE; +import static de.jottyfan.camporganizer.db.jooq.Tables.T_PROFILEROLE; +import static de.jottyfan.camporganizer.db.jooq.Tables.T_RSS; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.UUID; @@ -18,11 +21,17 @@ import org.jooq.DeleteConditionStep; import org.jooq.InsertResultStep; import org.jooq.InsertValuesStep12; import org.jooq.InsertValuesStep13; +import org.jooq.InsertValuesStep2; +import org.jooq.Record; import org.jooq.Record1; +import org.jooq.Record2; +import org.jooq.Record5; import org.jooq.Record7; import org.jooq.SelectConditionStep; +import org.jooq.UpdateConditionStep; import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; +import org.jooq.types.DayToSecond; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -33,6 +42,7 @@ import de.jottyfan.camporganizer.db.jooq.tables.records.TCampRecord; import de.jottyfan.camporganizer.db.jooq.tables.records.TPersonRecord; import de.jottyfan.camporganizer.db.jooq.tables.records.TPersondocumentRecord; import de.jottyfan.camporganizer.db.jooq.tables.records.TProfileRecord; +import de.jottyfan.camporganizer.db.jooq.tables.records.TRssRecord; import de.jottyfan.camporganizer.module.common.BookingBean; import de.jottyfan.camporganizer.module.common.LambdaResultWrapper; @@ -102,7 +112,31 @@ public class RegistrationGateway { if (bean.getRegisterInKeycloak() && !loginNotYetInUse) { throw new DataAccessException("login already in use: " + bean.getLogin()); } - // TODO: check if teacher is at least 2 years older than the camp participants + + // TODO: move to bean validator instead: + // check for valid birthdate of teachers + LocalDateTime birthDate = bean.getBirthDate().atStartOfDay(); + + if (EnumCamprole.teacher.equals(bean.getCampRole())) { + SelectConditionStep> sql = jooq + // @formatter:off + .select(T_CAMP.MAX_AGE, + DSL.localDateTimeDiff(T_CAMP.DEPART, birthDate).as("teacherAge")) + .from(T_CAMP) + .where(T_CAMP.PK.eq(bean.getFkCamp())); + // @formatter:on + LOGGER.debug(sql.toString()); + Record r = sql.fetchOne(); + Integer minTeacherAge = r.get(T_CAMP.MAX_AGE) + 2; // by default, we need 2 years older teachers at least + DayToSecond currentTeacherAge = r.get("teacherAge", DayToSecond.class); + double totalYears = currentTeacherAge.getTotalDays() / 365.25; // in years + int years = (int) totalYears; + if (years < minTeacherAge) { + throw new DataAccessException("Als Mitarbeiter bist Du leider zu jung für diese Freizeit."); + } + } + // end of check + Integer fkProfile = null; if (loginNotYetInUse) { String oldPassword = new StrongPasswordEncryptor().encryptPassword(bean.getPassword()); @@ -120,6 +154,16 @@ public class RegistrationGateway { // @formatter:on LOGGER.debug(sql1.toString()); fkProfile = sql1.fetchOne().getPk(); + + InsertValuesStep2 sql2 = jooq + // @formatter:off + .insertInto(T_RSS, + T_RSS.MSG, + T_RSS.RECIPIENT) + .values(new StringBuilder(bean.getFullname()).append(" hat sich als Nutzer im CampOrganizer2 registriert.").toString(), "admin"); + // @formatter:on + LOGGER.debug("{}", sql2.toString()); + sql2.execute(); } else { SelectConditionStep> sql1 = DSL.using(t) // @formatter:off @@ -228,6 +272,32 @@ public class RegistrationGateway { public Integer removeBooking(Integer id) { LambdaResultWrapper lrw = new LambdaResultWrapper(); jooq.transaction(t -> { + SelectConditionStep> sql0 = DSL.using(t) + // @formatter:off + .select(T_PROFILE.USERNAME, T_PERSON.FORENAME, T_PERSON.SURNAME, T_CAMP.NAME, T_CAMP.ARRIVE) + .from(T_PERSON) + .leftJoin(T_CAMP).on(T_CAMP.PK.eq(T_PERSON.FK_CAMP)) + .leftJoin(T_PROFILE).on(T_PROFILE.PK.eq(T_PERSON.FK_PROFILE)) + .where(T_PERSON.PK.eq(id)); + // @formatter:on + LOGGER.debug(sql0.toString()); + Record5 r = sql0.fetchOne(); + if (r == null) { + throw new DataAccessException("no such entry in t_person with id = " + id); + } + String username = r.get(T_PROFILE.USERNAME); + String forename = r.get(T_PERSON.FORENAME); + String surname = r.get(T_PERSON.SURNAME); + String campname = r.get(T_CAMP.NAME); + LocalDateTime arrive = r.get(T_CAMP.ARRIVE); + + StringBuilder rssMessage = new StringBuilder(username); + rssMessage.append(" hat die Buchung von "); + rssMessage.append(forename).append(" ").append(surname); + rssMessage.append(" an "); + rssMessage.append(campname).append(" ").append(arrive == null ? "" : arrive.format(DateTimeFormatter.ofPattern("YYYY"))); + rssMessage.append(" storniert."); + DeleteConditionStep sql1 = DSL.using(t).deleteFrom(T_PERSONDOCUMENT) .where(T_PERSONDOCUMENT.FK_PERSON.eq(id)); LOGGER.debug(sql1.toString()); @@ -236,6 +306,16 @@ public class RegistrationGateway { DeleteConditionStep sql2 = DSL.using(t).deleteFrom(T_PERSON).where(T_PERSON.PK.eq(id)); LOGGER.debug(sql2.toString()); lrw.add(sql2.execute()); + + InsertValuesStep2 sql3 = DSL.using(t) + // @formatter:off + .insertInto(T_RSS, + T_RSS.MSG, + T_RSS.RECIPIENT) + .values(rssMessage.toString(), "registrator"); + // @formatter:on + LOGGER.debug("{}", sql3.toString()); + sql3.execute(); }); return lrw.getCounter(); } @@ -261,4 +341,55 @@ public class RegistrationGateway { } return false; } + + /** + * remove login + * + * @param bean + * containing username of dataset to be removed + * @throws DataAccessExceptionF + */ + public void removeLogin(ProfileBean bean) throws DataAccessException { + jooq.transaction(t -> { + UpdateConditionStep sql = DSL.using(t) + // @formatter:off + .update(T_PERSON) + .set(T_PERSON.FK_PROFILE, (Integer) null) + .where(T_PERSON.FK_PROFILE.eq(bean.getPk())); + // @formatter:off + LOGGER.debug("{}", sql.toString()); + sql.execute(); + + DeleteConditionStep sql1 = DSL.using(t) + // @formatter:off + .deleteFrom(T_PROFILEROLE) + .where(T_PROFILEROLE.FK_PROFILE.in( + DSL.using(t) + .select(T_PROFILE.PK) + .from(T_PROFILE) + .where(T_PROFILE.USERNAME.eq(bean.getUsername()) + ))); + // @formatter:on + LOGGER.debug("{}", sql1.toString()); + sql1.execute(); + + DeleteConditionStep sql2 = DSL.using(t) + // @formatter:off + .deleteFrom(T_PROFILE) + .where(T_PROFILE.USERNAME.eq(bean.getUsername())); + // @formatter:on + LOGGER.debug("{}", sql2.toString()); + sql2.execute(); + + InsertValuesStep2 sql3 = DSL.using(t) + // @formatter:off + .insertInto(T_RSS, + T_RSS.MSG, + T_RSS.RECIPIENT) + .values(new StringBuilder(bean.getFullname()).append(" hat sich vom Portal CampOrganizer2 abgemeldet.").toString(), "admin"); + // @formatter:on + LOGGER.debug("{}", sql3.toString()); + sql3.execute(); + }); + } } diff --git a/src/main/java/de/jottyfan/camporganizer/module/rss/RssBean.java b/src/main/java/de/jottyfan/camporganizer/module/rss/RssBean.java new file mode 100644 index 0000000..b37de6f --- /dev/null +++ b/src/main/java/de/jottyfan/camporganizer/module/rss/RssBean.java @@ -0,0 +1,54 @@ +package de.jottyfan.camporganizer.module.rss; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * + * @author jotty + * + */ +public class RssBean implements Serializable { + private static final long serialVersionUID = 1L; + + public final Integer pk; + public String recipient; + public String message; + public LocalDateTime pubdate; + + public RssBean(Integer pk) { + this.pk = pk; + } + + public String getMessage80() { + return message == null ? null : (message.length() > 80 ? message.substring(0, 80).concat("...") : message); + } + + public Integer getPk() { + return pk; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public LocalDateTime getPubdate() { + return pubdate; + } + + public void setPubdate(LocalDateTime pubdate) { + this.pubdate = pubdate; + } + + public String getRecipient() { + return recipient; + } + + public void setRecipient(String recipient) { + this.recipient = recipient; + } +} diff --git a/src/main/java/de/jottyfan/camporganizer/module/rss/RssController.java b/src/main/java/de/jottyfan/camporganizer/module/rss/RssController.java new file mode 100644 index 0000000..773c917 --- /dev/null +++ b/src/main/java/de/jottyfan/camporganizer/module/rss/RssController.java @@ -0,0 +1,55 @@ +package de.jottyfan.camporganizer.module.rss; + +import java.io.IOException; +import java.io.PrintWriter; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +import com.rometools.rome.feed.synd.SyndFeed; +import com.rometools.rome.io.FeedException; +import com.rometools.rome.io.SyndFeedOutput; + +/** + * + * @author jotty + * + */ +@Controller +public class RssController { + + private String recipientCode; + + @Autowired + private RssService service; + + @GetMapping("/rss") + public String toRss(HttpServletResponse response) throws IOException, FeedException { + List beans = new ArrayList<>(); + if (recipientCode != null) { + beans = service.getRss(recipientCode); + } else { + RssBean bean = new RssBean(null); + bean.setPubdate(LocalDateTime.now()); + bean.setMessage("Dieser Feed ist nicht mehr aktuell. Bitte gib einen recipientCode an."); + beans.add(bean); + } + SyndFeed feed = new RssModel().getRss(beans); + response.reset(); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/rss+xml"); + response.setHeader("Content-Disposition", "attachment; filename=\"onkelwernerfreizeiten.de.xml\""); + PrintWriter writer; + writer = response.getWriter(); + SyndFeedOutput output = new SyndFeedOutput(); + output.output(feed, writer); + response.flushBuffer(); + return "error"; + } +} diff --git a/src/main/java/de/jottyfan/camporganizer/module/rss/RssGateway.java b/src/main/java/de/jottyfan/camporganizer/module/rss/RssGateway.java new file mode 100644 index 0000000..a19802a --- /dev/null +++ b/src/main/java/de/jottyfan/camporganizer/module/rss/RssGateway.java @@ -0,0 +1,101 @@ +package de.jottyfan.camporganizer.module.rss; + +import static de.jottyfan.camporganizer.db.jooq.Tables.T_RSS; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jooq.DSLContext; +import org.jooq.DeleteConditionStep; +import org.jooq.Record3; +import org.jooq.Record4; +import org.jooq.SelectConditionStep; +import org.jooq.SelectJoinStep; +import org.jooq.UpdateConditionStep; +import org.jooq.exception.DataAccessException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import de.jottyfan.camporganizer.db.jooq.tables.records.TRssRecord; + +/** + * + * @author jotty + * + */ +@Repository +@Transactional(transactionManager = "transactionManager") +public class RssGateway { + + private static final Logger LOGGER = LogManager.getLogger(RssGateway.class); + + @Autowired + private DSLContext jooq; + + public List getRss(String recipientCode) throws DataAccessException { + SelectConditionStep> sql = jooq + // @formatter:off + .select(T_RSS.PK, + T_RSS.MSG, + T_RSS.REGDATE) + .from(T_RSS) + .where(T_RSS.RECIPIENT.eq(recipientCode)); + // @formatter:on + LOGGER.debug("{}", sql.toString()); + List list = new ArrayList<>(); + for (Record3 r : sql.fetch()) { + RssBean bean = new RssBean(r.get(T_RSS.PK)); + bean.setRecipient(recipientCode); + bean.setMessage(r.get(T_RSS.MSG)); + bean.setPubdate(r.get(T_RSS.REGDATE)); + list.add(bean); + } + return list; + } + + public List getAllRss() throws DataAccessException { + SelectJoinStep> sql = jooq + // @formatter:off + .select(T_RSS.PK, + T_RSS.RECIPIENT, + T_RSS.MSG, + T_RSS.REGDATE) + .from(T_RSS); + // @formatter:on + LOGGER.debug("{}", sql.toString()); + List list = new ArrayList<>(); + for (Record4 r : sql.fetch()) { + RssBean bean = new RssBean(r.get(T_RSS.PK)); + bean.setRecipient(r.get(T_RSS.RECIPIENT)); + bean.setMessage(r.get(T_RSS.MSG)); + bean.setPubdate(r.get(T_RSS.REGDATE)); + list.add(bean); + } + return list; + } + + public void deleteRss(RssBean bean) throws DataAccessException { + DeleteConditionStep sql = jooq + // @formatter:off + .deleteFrom(T_RSS) + .where(T_RSS.PK.eq(bean.getPk())); + // @formatter:on + LOGGER.debug("{}", sql.toString()); + sql.execute(); + } + + public void update(RssBean bean) throws DataAccessException { + UpdateConditionStep sql = jooq + // @formatter:off + .update(T_RSS) + .set(T_RSS.MSG, bean.getMessage()) + .where(T_RSS.PK.eq(bean.getPk())); + // @formatter:on + LOGGER.debug("{}", sql.toString()); + sql.execute(); + } +} diff --git a/src/main/java/de/jottyfan/camporganizer/module/rss/RssModel.java b/src/main/java/de/jottyfan/camporganizer/module/rss/RssModel.java new file mode 100644 index 0000000..9abfc30 --- /dev/null +++ b/src/main/java/de/jottyfan/camporganizer/module/rss/RssModel.java @@ -0,0 +1,44 @@ +package de.jottyfan.camporganizer.module.rss; + +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; + +import com.rometools.rome.feed.synd.SyndContent; +import com.rometools.rome.feed.synd.SyndContentImpl; +import com.rometools.rome.feed.synd.SyndEntry; +import com.rometools.rome.feed.synd.SyndEntryImpl; +import com.rometools.rome.feed.synd.SyndFeed; +import com.rometools.rome.feed.synd.SyndFeedImpl; + +/** + * + * @author jotty + * + */ +public class RssModel { + public SyndFeed getRss(List beans) { + SyndFeed feed = new SyndFeedImpl(); + feed.setFeedType("rss_2.0"); + feed.setTitle("Onkel Werner Freizeiten e.V. Anmeldungsnotifier"); + feed.setLink("https://www.onkelwernerfreizeiten.de/camporganizer/rss.jsf"); + feed.setDescription("In diesem Feed werden Portalaktivitäten gesammelt."); + feed.setEncoding("UTF-8"); + List entries = new ArrayList<>(); + for (RssBean bean : beans) { + SyndEntry entry = new SyndEntryImpl(); + entry.setTitle("neue Aktivität"); + entry.setLink("https://www.onkelwernerfreizeiten.de/camporganizer/"); + entry.setUri(new SimpleDateFormat("yyyyMMddHHmmssSSS").format(bean.getPubdate())); + entry.setPublishedDate(Timestamp.valueOf(bean.getPubdate())); + SyndContent description = new SyndContentImpl(); + description.setType("text/plain"); + description.setValue(bean.getMessage()); + entry.setDescription(description); + entries.add(entry); + } + feed.setEntries(entries); + return feed; + } +} diff --git a/src/main/java/de/jottyfan/camporganizer/module/rss/RssService.java b/src/main/java/de/jottyfan/camporganizer/module/rss/RssService.java new file mode 100644 index 0000000..499e4a8 --- /dev/null +++ b/src/main/java/de/jottyfan/camporganizer/module/rss/RssService.java @@ -0,0 +1,27 @@ +package de.jottyfan.camporganizer.module.rss; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * + * @author jotty + * + */ +@Service +public class RssService { + @Autowired + private RssGateway repository; + + /** + * get the recipient's rss feed + * @param recipientCode the code for the feed + * @return the list of rss beans; an empty list at least + */ + public List getRss(String recipientCode) { + return repository.getRss(recipientCode); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6e06c2f..8ba7828 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -18,6 +18,16 @@ keycloak.use-resource-role-mappings = ${keycloak.use-resource-role-mappings} ow.keycloak.admin.name = ${ow.keycloak.admin.name} ow.keycloak.admin.password = ${ow.keycloak.admin.password} +spring.mail.default-encoding = ${spring.mail.default-encoding} +spring.mail.host = ${spring.mail.host} +spring.mail.username = ${spring.mail.username} +spring.mail.password = ${spring.mail.password} +spring.mail.port = ${spring.mail.port} +spring.mail.protocol = ${spring.mail.protocol} +spring.mail.test-connection = ${spring.mail.test-connection} +spring.mail.properties.mail.smtp.auth = ${spring.mail.properties.mail.smtp.auth} +spring.mail.properties.mail.smtp.starttls.enable = ${spring.mail.properties.mail.smtp.starttls.enable} + # for development only server.port = 8081