rss and mail

This commit is contained in:
Jottyfan 2022-12-17 17:34:10 +01:00
parent 2ecd3dbc38
commit 028c4d4fd4
22 changed files with 984 additions and 67 deletions

View File

@ -1,13 +1,2 @@
arguments=
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir= connection.project.dir=
eclipse.preferences.version=1 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

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project-modules id="moduleCoreId" project-version="1.5.0"> <project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="camporganizer2"> <wb-module deploy-name="CampOrganizer2">
<property name="context-root" value="camporganizer2"/> <property name="context-root" value="CampOrganizer2"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/resources"/> <wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/resources"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/java"/> <wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/java"/>
</wb-module> </wb-module>

View File

@ -63,6 +63,13 @@ dependencies {
// backward compatibility until the complete registration is converted to keycloak // backward compatibility until the complete registration is converted to keycloak
implementation 'org.jasypt:jasypt:1.9.3' 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-jooq'
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

View File

@ -16,13 +16,11 @@ import org.jooq.DSLContext;
import org.jooq.Record; import org.jooq.Record;
import org.jooq.SelectSeekStep1; import org.jooq.SelectSeekStep1;
import org.jooq.SelectSeekStep2; import org.jooq.SelectSeekStep2;
import org.jooq.UpdateConditionStep;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import de.jottyfan.camporganizer.db.jooq.tables.TProfile; 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; import de.jottyfan.camporganizer.db.jooq.tables.records.VCampRecord;
/** /**
@ -132,28 +130,4 @@ public class IndexGateway {
} }
return list; 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<TPersonRecord> 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();
}
} }

View File

@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import de.jottyfan.camporganizer.db.jooq.tables.records.VCampRecord; 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 @Service
public class IndexService { public class IndexService {
@Autowired @Autowired
private IndexGateway gateway; private IndexGateway gateway;
@Autowired
private DashboardGateway dashboardGateway;
/** /**
* get all camps from the database and prepare them for the view * get all camps from the database and prepare them for the view
* *
@ -54,6 +59,6 @@ public class IndexService {
* @return true or false * @return true or false
*/ */
public Boolean update(BookingBean bean) { public Boolean update(BookingBean bean) {
return gateway.update(bean) == 1; return dashboardGateway.update(bean) == 1;
} }
} }

View File

@ -1,5 +1,8 @@
package de.jottyfan.camporganizer.module.common; package de.jottyfan.camporganizer.module.common;
import java.util.HashMap;
import java.util.Map;
/** /**
* *
* @author henkej * @author henkej
@ -7,8 +10,12 @@ package de.jottyfan.camporganizer.module.common;
*/ */
public class LambdaResultWrapper { public class LambdaResultWrapper {
private Integer counter; private Integer counter;
private Map<String, Boolean> mapBoolean;
private Map<String, String> mapString;
public LambdaResultWrapper() { public LambdaResultWrapper() {
this.mapBoolean = new HashMap<>();
this.mapString = new HashMap<>();
counter = 0; counter = 0;
} }
@ -19,4 +26,20 @@ public class LambdaResultWrapper {
public void add(Integer i) { public void add(Integer i) {
counter += 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);
}
} }

View File

@ -25,9 +25,10 @@ public interface IPersonService {
* update bean in the database * update bean in the database
* *
* @param bean the bean * @param bean the bean
* @param worker the user that is doing the changes
* @return number of affected database rows * @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 * get all camps from the database that the user has access to

View File

@ -42,7 +42,8 @@ public class PersonController {
@PostMapping("/confirmation/person/update") @PostMapping("/confirmation/person/update")
public String doUpdate(@ModelAttribute PersonBean bean, Model model) { public String doUpdate(@ModelAttribute PersonBean bean, Model model) {
personService.updatePerson(bean); String username = confirmationService.getCurrentUser(request);
personService.updatePerson(bean, username);
return "redirect:/confirmation"; return "redirect:/confirmation";
} }
} }

View File

@ -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_LOCATION;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSON; 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_PROFILE;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_RSS;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext; import org.jooq.DSLContext;
import org.jooq.InsertValuesStep2;
import org.jooq.Record; import org.jooq.Record;
import org.jooq.Record11; import org.jooq.Record11;
import org.jooq.Record4; import org.jooq.Record4;
import org.jooq.SelectConditionStep; import org.jooq.SelectConditionStep;
import org.jooq.SelectSeekStep1; import org.jooq.SelectSeekStep1;
import org.jooq.UpdateConditionStep; import org.jooq.UpdateConditionStep;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; 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.EnumCamprole;
import de.jottyfan.camporganizer.db.jooq.enums.EnumModule; import de.jottyfan.camporganizer.db.jooq.enums.EnumModule;
import de.jottyfan.camporganizer.db.jooq.tables.TProfile; 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.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 @Autowired
private DSLContext jooq; private DSLContext jooq;
@Autowired
private MailRepository mailRepository;
/** /**
* get all camps from the database if username is allowed to maintain it * get all camps from the database if username is allowed to maintain it
* *
@ -125,26 +137,119 @@ public class PersonGateway {
* @param bean the bean * @param bean the bean
* @return the number of affected database rows * @return the number of affected database rows
*/ */
public Integer updatePerson(PersonBean bean) { public Integer updatePerson(PersonBean bean, String registrator) {
UpdateConditionStep<TPersonRecord> sql = jooq LambdaResultWrapper lrw = new LambdaResultWrapper();
// @formatter:off jooq.transaction(t -> {
.update(T_PERSON)
.set(T_PERSON.FORENAME, bean.getForename()) // get old accept value for comparison
.set(T_PERSON.SURNAME, bean.getSurname()) SelectConditionStep<TPersonRecord> sql = jooq.selectFrom(T_PERSON).where(T_PERSON.PK.eq(bean.getPk()));
.set(T_PERSON.STREET, bean.getStreet()) LOGGER.debug(sql.toString());
.set(T_PERSON.ZIP, bean.getZip()) TPersonRecord r = sql.fetchOne();
.set(T_PERSON.CITY, bean.getCity()) lrw.putBoolean("acceptOld", r == null ? null : r.getAccept());
.set(T_PERSON.BIRTHDATE, bean.getBirthdate()) lrw.putBoolean("acceptNew", bean.getAccept());
.set(T_PERSON.SEX, bean.getSex()) Integer fkCamp = r == null ? null : r.getFkCamp();
.set(T_PERSON.PHONE, bean.getPhone()) String email = r.getEmail(); // use the old one, too
.set(T_PERSON.EMAIL, bean.getEmail()) lrw.putString("oldEmail", email);
.set(T_PERSON.COMMENT, bean.getComment())
.set(T_PERSON.ACCEPT, bean.getAccept()) SelectConditionStep<TCampRecord> sql0 = jooq.selectFrom(T_CAMP).where(T_CAMP.PK.eq(fkCamp));
.set(T_PERSON.CAMPROLE, bean.getCamprole()) LOGGER.debug(sql0.toString());
.where(T_PERSON.PK.eq(bean.getPk())); TCampRecord rc = sql0.fetchOne();
// @formatter:on String campName = rc == null ? null : rc.getName();
LOGGER.debug(sql.toString()); LocalDateTime arrive = rc == null ? null : rc.getArrive();
return sql.execute(); String campNameWithYear = new StringBuilder(campName == null ? "" : campName).append(" ")
.append(arrive == null ? "" : arrive.format(DateTimeFormatter.ofPattern("YYYY"))).toString();
lrw.putString("campNameWithYear", campNameWithYear);
UpdateConditionStep<TPersonRecord> 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<TRssRecord, String, String> 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<String> 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();
} }
/** /**

View File

@ -23,8 +23,8 @@ public class PersonService implements IPersonService {
} }
@Override @Override
public Integer updatePerson(PersonBean bean) { public Integer updatePerson(PersonBean bean, String worker) {
return gateway.updatePerson(bean); return gateway.updatePerson(bean, worker);
} }
@Override @Override

View File

@ -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<TPersonRecord> 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<TPersondocumentRecord> 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<TRssRecord, String, String> 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<TPersondocumentRecord, String, EnumFiletype, Integer, String> 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<TRssRecord, String, String> 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();
});
}
}

View File

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

View File

@ -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<String> 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<String> 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;
}
}

View File

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

View File

@ -49,6 +49,13 @@ public class RegistrationBean implements Serializable {
private String login; private String login;
private String password; 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 * @return the forename
*/ */

View File

@ -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_PERSON;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_PERSONDOCUMENT; 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_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.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.UUID; import java.util.UUID;
@ -18,11 +21,17 @@ import org.jooq.DeleteConditionStep;
import org.jooq.InsertResultStep; import org.jooq.InsertResultStep;
import org.jooq.InsertValuesStep12; import org.jooq.InsertValuesStep12;
import org.jooq.InsertValuesStep13; import org.jooq.InsertValuesStep13;
import org.jooq.InsertValuesStep2;
import org.jooq.Record;
import org.jooq.Record1; import org.jooq.Record1;
import org.jooq.Record2;
import org.jooq.Record5;
import org.jooq.Record7; import org.jooq.Record7;
import org.jooq.SelectConditionStep; import org.jooq.SelectConditionStep;
import org.jooq.UpdateConditionStep;
import org.jooq.exception.DataAccessException; import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL; import org.jooq.impl.DSL;
import org.jooq.types.DayToSecond;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; 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.TPersonRecord;
import de.jottyfan.camporganizer.db.jooq.tables.records.TPersondocumentRecord; 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.TProfileRecord;
import de.jottyfan.camporganizer.db.jooq.tables.records.TRssRecord;
import de.jottyfan.camporganizer.module.common.BookingBean; import de.jottyfan.camporganizer.module.common.BookingBean;
import de.jottyfan.camporganizer.module.common.LambdaResultWrapper; import de.jottyfan.camporganizer.module.common.LambdaResultWrapper;
@ -102,7 +112,31 @@ public class RegistrationGateway {
if (bean.getRegisterInKeycloak() && !loginNotYetInUse) { if (bean.getRegisterInKeycloak() && !loginNotYetInUse) {
throw new DataAccessException("login already in use: " + bean.getLogin()); 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<Record2<Integer, DayToSecond>> 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; Integer fkProfile = null;
if (loginNotYetInUse) { if (loginNotYetInUse) {
String oldPassword = new StrongPasswordEncryptor().encryptPassword(bean.getPassword()); String oldPassword = new StrongPasswordEncryptor().encryptPassword(bean.getPassword());
@ -120,6 +154,16 @@ public class RegistrationGateway {
// @formatter:on // @formatter:on
LOGGER.debug(sql1.toString()); LOGGER.debug(sql1.toString());
fkProfile = sql1.fetchOne().getPk(); fkProfile = sql1.fetchOne().getPk();
InsertValuesStep2<TRssRecord, String, String> 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 { } else {
SelectConditionStep<Record1<Integer>> sql1 = DSL.using(t) SelectConditionStep<Record1<Integer>> sql1 = DSL.using(t)
// @formatter:off // @formatter:off
@ -228,6 +272,32 @@ public class RegistrationGateway {
public Integer removeBooking(Integer id) { public Integer removeBooking(Integer id) {
LambdaResultWrapper lrw = new LambdaResultWrapper(); LambdaResultWrapper lrw = new LambdaResultWrapper();
jooq.transaction(t -> { jooq.transaction(t -> {
SelectConditionStep<Record5<String, String, String, String, LocalDateTime>> 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<String, String, String, String, LocalDateTime> 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<TPersondocumentRecord> sql1 = DSL.using(t).deleteFrom(T_PERSONDOCUMENT) DeleteConditionStep<TPersondocumentRecord> sql1 = DSL.using(t).deleteFrom(T_PERSONDOCUMENT)
.where(T_PERSONDOCUMENT.FK_PERSON.eq(id)); .where(T_PERSONDOCUMENT.FK_PERSON.eq(id));
LOGGER.debug(sql1.toString()); LOGGER.debug(sql1.toString());
@ -236,6 +306,16 @@ public class RegistrationGateway {
DeleteConditionStep<TPersonRecord> sql2 = DSL.using(t).deleteFrom(T_PERSON).where(T_PERSON.PK.eq(id)); DeleteConditionStep<TPersonRecord> sql2 = DSL.using(t).deleteFrom(T_PERSON).where(T_PERSON.PK.eq(id));
LOGGER.debug(sql2.toString()); LOGGER.debug(sql2.toString());
lrw.add(sql2.execute()); lrw.add(sql2.execute());
InsertValuesStep2<TRssRecord, String, String> 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(); return lrw.getCounter();
} }
@ -261,4 +341,55 @@ public class RegistrationGateway {
} }
return false; 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<TPersonRecord> 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<TRssRecord, String, String> 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();
});
}
} }

View File

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

View File

@ -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<RssBean> 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";
}
}

View File

@ -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<RssBean> getRss(String recipientCode) throws DataAccessException {
SelectConditionStep<Record3<Integer, String, LocalDateTime>> 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<RssBean> list = new ArrayList<>();
for (Record3<Integer, String, LocalDateTime> 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<RssBean> getAllRss() throws DataAccessException {
SelectJoinStep<Record4<Integer, String, String, LocalDateTime>> 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<RssBean> list = new ArrayList<>();
for (Record4<Integer, String, String, LocalDateTime> 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<TRssRecord> 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<TRssRecord> 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();
}
}

View File

@ -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<RssBean> 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<SyndEntry> 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;
}
}

View File

@ -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<RssBean> getRss(String recipientCode) {
return repository.getRss(recipientCode);
}
}

View File

@ -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.name = ${ow.keycloak.admin.name}
ow.keycloak.admin.password = ${ow.keycloak.admin.password} 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 # for development only
server.port = 8081 server.port = 8081